Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Multiversal Sheet Worker Generator - this is bonkers!

1565013497

Edited 1584848657
GiGs
Pro
Sheet Author
API Scripter
Are you sick and tired of making sheet workers? Of copying the same sheet worker over and over, changing just a few things in the first and last lines? Would you like to add any number of sheet workers with just a single line of easy to read code for each? Have I got the tool for you! Call now for your special offer, terms & conditions may apply, don't read the small print. Seriously though, I am really excited with my newest tool. I already created the wiki page Universal Sheet Workers, to explain some ways to streamline making repetitive sheet workers, but that can be pretty clunky and impenetrable to people not used to heavy coding. But this tool - this is (I think) simpler to use, and more powerful, so takes that to a whole new dimension. Hence the silly name Multiversal Sheet Workers! This is a script that you drop into any character sheet's script block, of around 100 lines of code. Then once in, it generates sheet workers for you. You simply fill two variables: multistats, and multifunctions, with some information (described later), and it builds the sheet workers for you. In my test sheet, I replaced 120 sheet workers, totalling around 800 lines of code, with about 50 lines of instructions in total. Of course, I was able to use loops, arrow functions, and various other tricks to handle multiple sheet workers with a single line, but even at 1-2 lines per sheet worker, it would be a huge saving of work. I've reserved the following posts to explain how to use it. Once I've refined it through testing, and people hopefully using it, I'll add it to the wiki. A quick summary:  add all the code from post into your sheet's script block add attribute lists to the multistats variable. add functions to the multifunction variable. and you're done.  Post 2 is the Multiversal code. Post 3 is a reference post, describing briefly how to set up the two special objects (multistats and multifunctions). And also things to avoid, when you shouldnt use multiversals, etc. Post 4 explains the multistats object, again with detailed examples.  Post 5  explains the multifunction object in detail, with examples. this is likely to be the scariest part of setup, but it's much simpler than it appears. This is also the post that includes a good explanation of what is happening under the hood. So if you read anything, the first half of the post is a good start. Post 6: a hypothetical solution of how you would convert an entire character sheet to sheet workers, using advanced tips to make the code really short. It describes how to name your attributes to get the best out of this. Post 7 is reserved for notes on version updates, or any extra details i think off that dont fit in the previous posts. I'm  really  hoping people find this as useful as I do :) PS: jump to Wes's post for a great example of how to use this script, showing how simple it is.
1565013514

Edited 1565132272
GiGs
Pro
Sheet Author
API Scripter
The Code Block for your sheets, Version 1 . /* ====================================================== BEGIN MULTIVERSAL SHEET WORKER GENERATOR ====================================================== Use this script to build sheet workers automatically. Place this section in your script block, preferably at the end. Fill the two data objects, multistats and multifunctions, with your details. VERSION 1. READMORE: <a href="https://app.roll20.net/forum/permalink/7664925/" rel="nofollow">https://app.roll20.net/forum/permalink/7664925/</a> ======================================================*/ const multistats = { }; const multifunctions = { sum: (arr) =&gt; arr.reduce((total, add) =&gt; total + add), // add up any number of attributes &nbsp; &nbsp; pick_from_list: (arr) =&gt; arr[ arr[0] ], // get the value of an attribute, from a select. }; // ======================================================*/ // DO NOT EDIT BELOW THIS LINE // // ======================================================*/ const mvlog = (title, text, color = 'green', style='font-size:12px; font-weight:normal;', headerstyle = 'font-size:13px; font-weight:bold;') =&gt; { let titleStyle = `color:${color}; ${headerstyle} text-decoration:underline;`; let textStyle = `color:${color}; ${style}`; const output = `%c${title}:%c ${text}`; console.log(output,titleStyle,textStyle); }; // can use $ placeholder in attribute names. This converts '$_stat' to 'repeating_section_stat' const rep = '$'; //placeholder for repeating_section_ const makeRepeatingName = (attribute, section) =&gt; attribute.startsWith(rep) ? attribute.replace(rep, `repeating_${section}_`) : attribute; const makeRepeatingAttributes = (attributes, section) =&gt; attributes.map(a =&gt; makeRepeatingName(a, section)); const makeRepeatingID = (a, section, id) =&gt; a.replace(`repeating_${section}_`,`repeating_${section}_${id}_`); // given array of attributes, find if any have repeating_ and return the section name // section name will be 2nd element of name split on "_" const findSection = (arr) =&gt; { const s = arr.find(a =&gt; a.includes('repeating_')); const section = (s ? s.split('_')[1] : null); return section; }; // check if attribute is one where a repeating section attribute depends on attributes both inside and outside the repeating section const isMixed = (attributes, destination) =&gt; { const some = someRepeating(attributes); const all = allRepeating(attributes); const repeatingdestination = destination.startsWith('repeating_'); return (some &amp;&amp; !all &amp;&amp; repeatingdestination); }; const allRepeating = attributes =&gt; attributes.every(r =&gt; r.startsWith('repeating_')); const someRepeating = attributes =&gt; attributes.some(r =&gt; r.startsWith('repeating_')); const defaultDataType = 'array'; // might change this to object const getData = (values, data = 'a', isnumbers = 0) =&gt; { // only a is functional right now, so this function is redundant. switch(data.charAt(0).toLowerCase()) { case 'o': return values; case 'a': return Object.values(values).map(i =&gt; 1 === isnumbers ? parseInt(i) ||0 : (0 === isnumbers ? +i || 0 : i)); case 'v': return Object.values(values)[0]; } }; const processMax = (destination, result, max) =&gt; { const settings = {}; if(max === 'current' || max === 'both') settings[destination] = result; if(max === 'max' || max === 'both') settings[`${destination}_max`] = result; return settings; }; const isFunction = value =&gt; value &amp;&amp; (Object.prototype.toString.call(value) === '[object Function]' || 'function' === typeof value || value instanceof Function); const processFunction = (destination, values, section) =&gt; { const rule = multistats[destination].rule; const func = isFunction(rule) ? rule: multifunctions[rule]; // need to test if this works for arrow functions const data = multistats[destination].data || defaultDataType; const v = getData(values, data); const modifier = multistats[destination].modifier || null; const result = func(v, modifier); mvlog(`${makeRepeatingName(destination,section).toUpperCase()} MULTIFUNCTION`, `RULE: ${rule}; VALUES: ${JSON.stringify(values)}; RESULT: ${result}`); return result; }; Object.keys(multistats).forEach(destination =&gt; { // get the section name if it exists. It is needed for mixed workers const attributes_base = multistats[destination].attributes; const section = multistats[destination].section || findSection(attributes_base) || null; const attributes = makeRepeatingAttributes(attributes_base, section); const realdestination = makeRepeatingName(destination, section); // needed in case of $ in destination mvlog(`MULTIVERSAL- ${realdestination}`,`${attributes.join(', ')}`,'green'); if (isMixed(attributes, realdestination)) { const changes = attributes.reduce((change, step) =&gt; `${change} change:${step.replace('repeating_' + section + '_','repeating_' +section + ':')}`, `remove:repeating_${section} sheet:opened`); on(changes.toLowerCase(), function (event) { const trigger = event.sourceAttribute || ''; const triggerRow = (trigger &amp;&amp; trigger.includes('_') &amp;&amp; trigger.length &gt;2) ? trigger.split('_')[2] : ''; // if triggerRow, only update initial row getSectionIDs(`repeating_${section}`, function (ids) { const sectionAtts = attributes.filter(f =&gt; f.startsWith(`repeating_${section}`)); const fixedAtts = attributes.filter(f =&gt; !f.startsWith(`repeating_${section}`)); if (triggerRow) ids = [triggerRow]; const fieldNames = ids.reduce( (m,id) =&gt; [...m, ...(sectionAtts.map(field =&gt; makeRepeatingID(field,section,id) ))],[]); getAttrs([...fieldNames,...fixedAtts], function (values) { let settings = {}; const max = multistats[destination].max || 'current'; const fixedValues = fixedAtts.reduce((obj, a) =&gt; { obj[a] = values[a]; return obj; }, {}); ids.forEach(id =&gt; { // first get all relevant attributes for this row of the section const sectionValues = sectionAtts.reduce((obj, a) =&gt; { const att = makeRepeatingID(a, section, id); obj[att] = values[att]; return obj;}, {}); // now apply the formula for this row and add to settings const combinedValues = {...sectionValues,...fixedValues}; const result = processFunction(destination,combinedValues, section); const tempDestination = makeRepeatingID(realdestination,section,id); const tempSettings = processMax(tempDestination, result, max); settings = Object.assign({}, settings, tempSettings); }); setAttrs(settings); }); }); }); } else { const changes = attributes.reduce((change, step) =&gt; `${change} change:${step.replace('repeating_' + section + '_','repeating_' +section + ':')}`, `${someRepeating([...attributes,realdestination]) ? '' : 'sheet:opened '}${section ? `remove:repeating_${section}` : ''}`); on(changes.toLowerCase(), function () { getAttrs(attributes, function (values) { const result = processFunction(destination,values, section); const max = multistats[destination].max || 'current'; const settings = processMax(realdestination, result, max); setAttrs(settings); }); }); } });
1565013525

Edited 1711302586
GiGs
Pro
Sheet Author
API Scripter
Here's a brief explanation of the function, and its parameters. There's more detail in the following posts. MULTISTATS Example: &nbsp; &nbsp; const multistats = { &nbsp; &nbsp; &nbsp; &nbsp; dex_total: {rule: sum, attributes: ['dex_base', 'dex_mod']}, &nbsp; &nbsp; &nbsp; &nbsp; '$weapons_attack': {rule: sum, attributes: ['dex_total', '$weapons_skill', '$weapons_quality'], section: weapons}, &nbsp; &nbsp; }; With multistats, you define which attributes will be updated by the Multiversal. There are three required elements: destination: {rule: something, attributes: [an array] } That's the minimum, there are some extra ones. They are listened in ALL CAPS below to make them standout, but in your code they should be lower case. DESTINATION: the attribute that will change. It does not need to be in quotes, unless it starts with a $ (see below). RULE: the name of the function used to update changes (see multifunctions below) ATTRIBUTES: an array of the attributes that are watched for changes, and whose values are used in the multifunction. &nbsp; &nbsp; &nbsp; &nbsp; attributes is the group of stats you'd normally put in the on(change) and getAttrs lines. &nbsp; &nbsp; &nbsp; &nbsp; If an attribute starts with $, it is assumed to be from a repeating section. It replaces the "repeating_section_" part of the name. &nbsp; &nbsp; &nbsp; &nbsp; In that case you also need: SECTION: [OPTIONAL] if any of the stats are in a repeating section, this is where you put the name of the section. MAX: [OPTIONAL] lets you set the max as well as, or instead of, the current score. &nbsp; &nbsp; &nbsp; &nbsp; values: 'current', 'max', or 'both'. if omitted, it sets 'current' VALUES: [OPTIONAL] does your function need the values as integers (1), floating point numbers (0), or strings (-1). &nbsp; &nbsp; &nbsp; &nbsp; allowed values: 1, 0, or -1. If omitted, 0 is default. The array of values will be all numbers.&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if you need different types (a string and some numbers, say), set as -1 (strings), and process them in the multifunction. &nbsp; &nbsp; &nbsp; &nbsp; note: if you want the final value to have a fixed number of decimal places, set that in the multifunction. MODIFIER: [OPTIONAL] some functions can take extra data to modify how they work - you need to enter them here. &nbsp; &nbsp; &nbsp; &nbsp; Will be described in more detail later, with some examples. &nbsp; &nbsp; &nbsp; &nbsp; Values can be any data type - string, number, array, object. etc., but your multifunction will have to interpret it. &nbsp; &nbsp; &nbsp; &nbsp; Its great for simple adjustments: say, function that calculates move rate, which needs x1, x2, or x3 for different speeds.&nbsp; DATA: [OPTIONAL] functions require data to be in object, array, or variable form. This feature isnt working yet, so don't use it. MULTIFUNCTIONS &nbsp; &nbsp; This is where the magic happens. You can define a variety of functions such as adding up all the values, or calculating an attribute bonus &nbsp; &nbsp; By default, a multifunction receives an array of the attribute values, in the order you defined them. &nbsp; &nbsp; For example, the dex_total multistat above sends an array of two attributes, the dex_base and dex_mod , in that order. &nbsp; &nbsp; You can access them with array notation: arr[0] is the first one, arr[1] is the second. &nbsp; &nbsp; For most functions, you know how many stats are supplied, and knowing the order means you can build&nbsp; &nbsp; &nbsp; If you dont know how many attributes will be called, you can use a loop to handle them all. LIMITATIONS &nbsp; &nbsp; Some sheet workers set multiple attribute values at once. This doesnt work for those. &nbsp; &nbsp; I recommend using lower case for all attribute names in your sheet. It's absolutely required &nbsp;for the repeating_stat &nbsp;part of repeating sections. For others, its not required, but it'll make your life easier. &nbsp; &nbsp; For calculations on entire repeating sections (like totalling up inventory), use repeatingSum or Aaron's TAS instead.
1565013532

Edited 1565022551
GiGs
Pro
Sheet Author
API Scripter
MULTISTATS, EXPLAINED A simple and short multistats variable might look like this const multistats = { &nbsp; &nbsp; silver_piece_value: {rule: 'calc_silver_value', attributes: ['gp', 'sp', 'cp']}, &nbsp; &nbsp; dex_score: {rule: 'sum', attributes: ['dex_base','dex_race','dex_level','dex_magic', 'dex_item']}, &nbsp; &nbsp; dex_bonus: {rule: 'dnd_stat_bonus', attributes: ['dex_score']}, hp: {rule: 'sum', attributes: ['con_score','level_bonus','feat_bonus','other_hp_bonus'], max: 'both'}, move_base: {rule: 'calc_speed', attributes: ['dex_score','race_move','special_move']}, move_run: {rule: 'multiply', attributes: ['move_base'], modifier: 2}, move_sprint: {rule: 'multiply', attributes: ['move_base'], modifier: 3} }; In the first line, silver_piece_value is the DESTINATION - this is the name of the attribute you would normally put in setAttrs, which will be updated by the sheet worker. rule: is the name of the function which will be used to calculate the result. That's covered in the Multifunction post. attributes: is the list of attributes that will be monitored for changes, and used in the function to calculate the value. And that's it! With just a destination, a rule, and one or more attributes, you have generated a sheet worker. (How rules are used will be covered in the next post.) Let's look through the above multistats object and examine each example. In the&nbsp; dex_score row, you can see a rule sum. This will be covered in the next post,&nbsp; &nbsp; but all you need to know for now is that it's a function that adds up all the listed attributes, no matter how many or few. So by just listing the attributes needed for dex_score, and a rule, a sheet worker is created that adds them and saves the result to dex_score. In the dex_bonus column, we use another rule, to calculate a stat bonus. So, whenever any of the base dex attributes are changed, the dex_score worker runs and updates its score, and then the dex_bonus worker runs, and update its value. It's that easy. Most games need a hp calculator. Since the stats listed are summed up, this uses the sum rule, and this time there's an extra parameter: max: 'both' . This means it calculates both a current and max score for hp - this is especially handy for stats you plan to put on a token bar, since a max is needed for the bar to work properly. Then we have three lines for movement speed. The rule in move_base&nbsp; names a function to calculate base speed. The move_run and move_sprint use the&nbsp; multiply rule with a modifier . Each of these takes the&nbsp; move_base attribute and multiplies it by 2 or 3. REPEATING SECTIONS You can handle repeating sections just as easily. Just supply attributes as you would normally. You can use the familiar syntax: repeating_section_attributename , or to save on typing can use $attributename &nbsp;(replacing the repating_section_ part with a $). If you use the shorthand version, you must supply a section: parameter. Here's a couple of examples: const multistats = { &nbsp; &nbsp; repeating_weapons_wpn_attackbonus: {rule: 'sum', attributes: ['repeating_weapons_wpn_skill', 'repeating_weapons_wpn_stat', 'repeating_weapons_wpn_bonus']}, &nbsp; &nbsp; '$wpn_attackbonus': {rule: 'sum', section: weapons, attributes: ['$wpn_skill', '$wpn_stat', '$wpn_bonus']}, }; The above two stat rules do the same thing. Notice when using the shorthand, the DESTINATION must be enclosed in quotes. const multistats = { &nbsp; &nbsp; '$wpn_attackbonus': {rule: 'sum', section: weapons, attributes: ['$wpn_skill', 'str_bonus', '$wpn_bonus']}, }; Here's a worker that uses an attribute from outside the repeating section, as well as two inside.&nbsp; It will monitor the str_bonus and the repeating stats for changes, and update when any of the stats change. If you change a repeating row, it updates just that row. If you change the str_bonus, it updates all the rows.&nbsp; If you have a stat selector within a repeating section (like a skill chosen via a select tag, and you choose which stat it applies to), this is a little trickier, but do-able. It's covered in advanced tricks below.
1565013539

Edited 1565024511
GiGs
Pro
Sheet Author
API Scripter
MULTIFUNCTIONS, EXPLAINED Before getting into the nitty gritty, lets describe how multistats and multifunctions interact. Imagine you have a sheet set up, with these multistats and multifunctions.&nbsp; Don't be intimidated by the multifunction, it's a lot simpler than it looks. Just bear with me. Now, imagine a player gets some money and adds 10 GP to their gp&nbsp; attribute. gp &nbsp;is in that first multistat, so the silver_piece_value sheet worker kicks into life. The first thing it does is look at the character sheet, and grab the values of GP, SP, and CP. let's say they are 13, 57, and 240 in that order. The multiversal creates an array containing these values, which is thus: attributes: [13, 57, 240] Now, it tries to figure out what to do with them, and looks at the rule for this worker. It sees a rule named gp_plus_sp_plus_cp &nbsp;and looks for a matching function in multifunctions: It has found one, and so grabs the function, which looks like this: function(arr) { var gp = arr[0]; var sp = arr[1]; var cp = arr[2]; var result = gp * 10 + sp *1 + cp /10; // calculate silver piece value. result = Math.round(result * 10)/10; // get one fixed decimal point. return result; }, It runs that function, replacing (arr) with [ 13, 57, 240] . So, it extracts gp 13, sp 57, and cp 240, runs the listed calculation, and gets 211 . It then sends that result back to multistats: And then the result is saved in the silver_piece_value stat. Some things to notice about this process The function didnt know the names of the stats it was manipulating. It just received an array of unidentified numbers. But it was able to calculate the correct GP, SP and CP values, because they are always listed in the array in the same order - the order you list them in multistats. So you can build your functions without ever needing to use this syntax: var stat = +values.stat || 0; The values are always passed as floating point numbers (3.1, 117.32, etc) , unless you specify differently using the values parameter, like so: silver_piece_value: {rule: 'calc_silver_value', attributes: ['gp', 'sp', 'cp'], values: 1}, values 1 = integer, values 0 = floating point number (the default), -1 = strings. Because values are anonymous, this means you can use the same function for many different sheet workers. For instance, imagine&nbsp; const multistats = { &nbsp; &nbsp; str_score: {rule: 'sum', attributes: ['str_base','str_race','str_level','str_magic', 'str_item']}, &nbsp; &nbsp; dex_score: {rule: 'sum', attributes: ['dex_base','dex_race','dex_level','dex_magic', 'dex_item']}, &nbsp; &nbsp; con_score: {rule: 'sum', attributes: ['con_base','con_race','con_level','con_magic', 'con_item']}, } Here we have a bunch of stat calculations, all using the same rule, sum . This function just adds together the numbers you send it, and then returns the result to be saved in the listed attribute. that means it works for any attribute where you just want to add together two or more numbers. That's probably well over half your sheet workers right there. Building Your Multifunctions So now you know the basic idea of how it works, how hard is it to build each needed function? Really, not that hard. The system supports whatever style of function writing you're familiar with, and I'll show a few examples. The one constant is that your function accepts an array of unidentified values, in a predefined order. You then build your function to take that array and do whatever you want with it, ending with a return value. So, here's four ways to build a sum function (and a more compact way to do that silver_piece_value function): const multifunctions = { sum_via_loop: function(arr) { let sum = 0; for(let i = 0; i &lt; arr.length; i++) { sum += arr[i]; } return sum; }, sum_foreach: function(arr) { let sum = 0; arr.forEach(i =&gt; { sum += i; }); return sum; }, sum_foreach_compact: function(arr) { let sum = 0; arr.forEach(i =&gt; sum += i); return sum; }, sum_via_reduce: (arr) =&gt; arr.reduce((total, add) =&gt; total + add), &nbsp;&nbsp;&nbsp;&nbsp;gp_plus_compact: (arr) =&gt; Math.round( (arr[0]*10 + arr[1] + arr[2]/10) *10)/10, }; This first one is likely a very familiar construction for anyone. You might normally write it like this: function sum_via_loop(arr) { let sum = 0; for(let i = 0; i &lt; arr.length; i++) { sum += arr[i]; } return sum; }; To make it a multifunction, all you have to do is move the name to before the word function function sum_via_loop(arr) { becomes sum_via_loop: function (arr) { and end it with a comma not a semicolon. (Don't forget this.) You can make all your functions using that syntax, but you can also use arrow functions which can make code much smaller (see the alternate silverpiece calculation and sum_via_reduce. All four of those sum functions do exactly the same thing. I just wanted to show you, you dont have to use fancy synax - you can use whatever syntax you are familiar with.&nbsp; Here are three functions to calculate a DnD Stat bonus. Note that this is calculated using just one stat, so you grab the first value in the arr (arr[0]): dnd_stat_bonus: function(arr) { var score = arr[0]; var bonus = Math.floor(score/2) -5; return bonus; }, dnd_stat_bonus_compact: function(arr) { return Math.floor(arr[0]/2) -5; }, dnd_bonus_by_arrow: (arr) =&gt; Math.floor(arr[0]/2 -5), dividefirstbysecond: function(arr) { var numerator = arr[0]; var denominator = arr[1]; var result = numerator / denominator; return result; } The last one is just a bit silly, but who knows maybe you'll need to divide one number by another. So thats the basic principle. I include the sum (reduce version) in the multiversal code, since its bar far the most common thing you do with sheet workers. Even just using that one function, and adding the multistats for your your sheet, you can probably eliminate a huge number of your sheet workers. I'll describe how to get fancy in the next post.
1565013545

Edited 1565133702
GiGs
Pro
Sheet Author
API Scripter
OPTIONAL PARAMETERS AND ADVANCED TRICKS In this section I'll describe some less obvious things you can do with multiversals.&nbsp; 1. GRABBING FROM A DROPDOWN This one's not so much an advanced tip, it's more of a workaround. One thing you'll often want to do, is get a value from a dropdown list. The best way when using multiversals is to set up your select value as below: &lt;select name="attr_selectname" &gt; &lt;option value='1' selected&gt;STR&lt;/option&gt; &lt;option value='2'&gt;DEX&lt;/option&gt; &lt;option value='3'&gt;CON&lt;/option&gt; &lt;option value='4'&gt;INT&lt;/option&gt; &lt;option value='5'&gt;WIS&lt;/option&gt; &lt;option value='6'&gt;CHA&lt;/option&gt; &lt;/select&gt; &lt;input type="hidden" name="attr_inputname" value="0" readonly&gt; You give each option a number, starting with 1. Make sure to give one option the selected tag, so that the select always has a value. You also create a linked input. This is to hold the value of the selected item. Then you'll be able to use @{inputname} whenever you want the selected value. If you select DEX, inputvalue will equal the dex bonus.&nbsp; To get this working, set up your multistats like this: const multistats = { &nbsp; &nbsp; inputname: {rule: 'pick_from_list', attributes: ['selectname', 'str_bonus', 'con_bonus', 'dex_bonus', 'int_bonus', 'wis_bonus', 'cha_bonus']}, }; You want your attribute list to start with the name of the select dropdown (in this case selectname), and follow with the rest of attributes in the select (in the same order). Now you can use the following multifunction: const multifunctions = { &nbsp; &nbsp; pick_from_list: function(arr){ var selected = arr[0]; var score_of_chosen = arr[selected]; return score_of_chosen; }; If you like to show off, you can write the function more concisely: const multifunctions = { &nbsp; &nbsp; pick_from_list: (arr) =&gt; arr[ arr[0] ], }; And that should do it. For extra credit, think through how it works. It can be a bit mindbending, if you're not familiar with the approach. It's a nice little puzzle.&nbsp; This technique works for any select/dropdown on your sheet, as long as the options values are numbers, and the attributes are in the same order as in the select. Since this is such a common thing to want to do, the multiversal script in post 2 now contains this function as standard, along with sum. 2. USING MODIFIERS Sometimes you want to reuse the same function with slight tweaks. For example, imagine your system has a move_speed attribute, which is calculated by (str + con)/4 . But you can run at x2 speed, and sprint at x3 speed, and you want to show those on the character sheet. You'd do that like this: const multistats = { &nbsp; &nbsp; move_speed: {rule: 'calculate_speed', attributes: ['dex_score', 'str_score'], modifier: 1}, move_run: {rule: 'multiply', attributes: ['move_speed'], modifier: 2}, move_sprint: {rule: 'multiply', attributes: ['move_speed'], modifier: 3}, }; now you need two functions, one to calculate the speed, and one to apply the multiples const multifunctions = { &nbsp; &nbsp; calculate_speed: function(arr) { &nbsp; &nbsp; &nbsp; &nbsp; let str = arr[0]; let con = arr[1]; let speed = Math.round((str+con)/4); &nbsp; &nbsp; &nbsp; &nbsp; return speed; &nbsp; &nbsp; }, multiply: function(arr, mult) { let speed = arr[0]; let speed_multipled = speed * mult; return speed_multipled; }, }; There are shorter ways to write these functions, but this way helps show what they are doing. When you have a modifier in multistats, you need to add a second label in the function(arr, something) line. You can name it whatever you want. You can then apply it in your function, using the label you gave it in the function line. So in this case, whenever dex or str change, the move_speed is calculated. and when move_speed changes, the run and sprint attributes are also updated. This is a very simple application of a modifier. The modifier can be a single number, a string, an array, any kind of data. It can't be another attribute though - any attributes you use in your function have to be defined in the attributes: group.&nbsp; 3. MIXING IT UP: STRINGS AND INTEGERS By default, multifunctions assume you are passing floating point numbers (numbers with decimals). Sometimes you'll want to use text, or integers. You can set a values parameter, setting it to 1 for integers, and -1 for strings (text). Lets say you are playing Pendragon, and have a textbox, in which you display the characters name, rank, age, glory, and their highest skill, in a format like: Sir Lance is a 29 year old Bachelor Knight, with 3,300 glory, who is known for his exceptional Battle skill, which has a score of 18. Assuming the character sheet has the attributes: character_name, age, rank, glory, and a list of skills, you could create the following: const multistats = { &nbsp; &nbsp; description: {rule: 'build_description', values: -1, attributes: ['character_name', 'age', 'rank', 'glory', (a long list of skills)]}, }; Note the values: -1, which means the attributes grabbed are left as strings. Even if it looks like a number, it's a string. This means if you want to perform calculations with the numbers, you'll have to the familiar code: let mynumber = parseInt(arr[1]) || 0; We dont have to do that here, just yet. So we need to build a function to output that previous text. Let's begin: const multistats = { build_description: function(arr) { &nbsp; &nbsp; &nbsp; &nbsp; let name = arr[0]; let age = arr[1]; let rank = arr[2]; let glory = arr[3]; &nbsp; &nbsp; &nbsp; &nbsp; let description = "Sir " + name + " is a " + age + " year old " + rank + ", with " + glory + " glory, who is known for his "; }, That handles the first part of the desired output. We could also write this using string literals: const multistats = { build_description: function(arr) { &nbsp; &nbsp; &nbsp; &nbsp; const description = `Sir ${arr[0]} is a ${arr[1]} year old ${arr[2]}, with ${arr[3]} glory, who is known for `; }, Either way, both have the same effect. use whichever suits you. Now, the second part: figuring out which skill has the highest value, and displaying it, is a bit tricky. I did originally have a solution here, but it was distracting from the purpose of this section. So I'll leave it as an exercise for the reader. How would you complete this function? :) The basic point though: if you want to use text, you need to use values: -1. 4. MULTIFUNCTIONS WITHIN MULTIFUNCTIONS In sheet workers, you can create functions, and call them in other functions. You can do this with multifunctions too, and it's simpler than you might think.&nbsp; Lets say you have a sum function. All it does is count up the total of the attributes sent to it. Now, you're playing Pendragon, and want to display the characters Chivalry Bonus. To qualify for this, knights knight a total of 80 points in six personality traits. If they have it, they get +3 armour; if not +0. You could create a function that adds up those 6 traits individually (arr[0] + arr[1] + arr[2], etc). Or you could recognise you are totalling them up, and you already have a sum function that does that. Why not use it? const multifunctions = { &nbsp; &nbsp; sum_via_loop: function(arr) { &nbsp; &nbsp; &nbsp; &nbsp; let sum = 0; &nbsp; &nbsp; &nbsp; &nbsp; for(let i = 0; i &lt; arr.length; i++) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sum += arr[i]; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; return sum; &nbsp; &nbsp; }, chivalry_bonus: function(arr) { let sum_of_chivalry_traits = multifunctions.sum_via_loop(arr); let armour = 0; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(sum_of_chivalry_traits &gt;= 80) armour = 3; return armour; }, }; Look at the sum_of_chivalry_traits line. In sheet workers, you normally call a function like this: var something = name_of_function(something_else); A multifunction works the same, you just put multifunction and a dot in front of it. Hence: var something = multifunction.name_of_function(something_else); It's that easy. FURTHER TIPS? These just scratch the surface of what you can do. I may come back and add new tips later. In the next post, I'll bring all this together and combine with the old universal sheet workers, to give an example of the full power of the script.
1565013555

Edited 1570381270
GiGs
Pro
Sheet Author
API Scripter
COMBINING UNIVERSAL SHEET WORKERS AND MULTIVERSALS I've put this in a separate post because it is much more complex than the preceding tips and should be regarded as strictly optional. You dont need this, but in projects with lots of skills, attributes, or other similar ratings, it can be very valuable. Over on the wiki, I described a technique to create multiple sheet workers in one go: Universal Sheet Workers . You can use the techniques described there to reduce the amount of duplication in multistats lists. For example, a few posts below this one, Wes shows his usage of Multiversals. I'll print part of it here for convenience: const multistats = { strength_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['strength', 'level']}, constitution_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['constitution', 'level']}, dexterity_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['dexterity', 'level']}, intelligence_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['intelligence', 'level']}, wisdom_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['wisdom', 'level']}, charisma_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['charisma', 'level']}, }; There we have six stats, but they all follow the same pattern. These are all identical except for the attribute names. So you could create a function to generate those.&nbsp; First initiate the multistats variable. it must exist first. const multistats = {}; Then create an array of the attributes: const stats = ['strength', 'consititution', 'dexterity', 'intelligence', 'wisdom', 'charisma']; Now create a loop to make the multistat list above stats.forEach(stat =&gt; { &nbsp; &nbsp; multistats[`${stat} _mod_level`] =&nbsp; {rule: 'dnd_stat_half_bonus', attributes: [stat, 'level']}; }); Be careful to use the slightly different syntax: an = sign instead of : to separate the multistat name and its value.&nbsp; And that's it - this code generates the multistats for all 6 attributes. Now, wouldnt it be nice if we can do the same for Wes Skills. Lets look at the first few: acrobatics_modifier: {rule: 'dnd_skill_bonus', attributes: ['dexterity_mod_level', 'acrobatics-trained', 'acrobatics-pen', 'acrobatics-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;arcana_modifier: {rule: 'dnd_skill_bonus', attributes: ['intelligence_mod_level', 'arcana-trained', 'arcana-pen', 'arcana-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;athletics_modifier: {rule: 'dnd_skill_bonus', attributes: ['strength_mod_level', 'athletics-trained', 'athletics-pen', 'athletics-misc']}, Straight away we can see there isn't a simple one-to-one relationship between the key (left side) and value (right side). Acrobatics depends on dexterity, arcana depends on intelligence, and so on. We could create 6 loops here, one for each attribute, which would look something like: const dex_skills = ['acrobatics','stealth', 'thievery']; dex_skills.forEach(skill =&gt; { &nbsp; &nbsp; multistats[`${skill} _modifier`] =&nbsp; {rule: 'dnd_skill_bonus', attributes: ['dexterity_mod_level', `${skill}-trained`, ` ${skill} -pen`, ` ${skill} -misc`]}; }); Creating a loop like that for each of the 6 stats would do the job and would be a bit more compact than the original list. But we can do better. We can create a an object to hold the full list of skills, and for each skill, give a value that is its linked attribute, like so: const skills = {acrobatics: 'dexterity',&nbsp; arcana: 'intelligence', athletics: 'strength', bluff:'charisma', diplomacy: 'charisma', dungeoneering: 'wisdom',&nbsp;endurance: 'constitution', heal: 'wisdom', history: 'intelligence', insight: 'wisdom', intimidate: 'charisma', nature: 'wisdom', perception: 'wisdom', religion: 'intelligence', stealth: 'dexterity', streetwise: 'charisma', thievery: 'dexterity' }; Here we have the full set of skills, listed with their appropriate attribute. We can loop through that using Object(entries) , like so: Object.entries(skills).forEach(([skill,stat]) =&gt; { multistats[`${skill}_modifier`] = {rule: 'dnd_skill_bonus', attributes: [`${stat}_mod_level`, `${skill}-trained`, `${skill}-pen`, `${skill}-misc`]}; }); This part: Object.entries(skills).forEach(([skill,stat]) loops through each 'entry' in the skills object, and separates them into two parts, the skill and stat.&nbsp; So, putting it all together, we can reduce 25 line multistats object to this: const multistats = (); const stats = ['strength', 'consititution', 'dexterity', 'intelligence', 'wisdom', 'charisma']; stats.forEach(stat =&gt; { &nbsp; &nbsp; multistats[`${stat}_mod_level`] =&nbsp;{rule: 'dnd_stat_half_bonus', attributes: [stat, 'level']}; }); const skills = {acrobatics: 'dexterity',&nbsp; arcana: 'intelligence', athletics: 'strength', bluff:'charisma', diplomacy: 'charisma', dungeoneering: 'wisdom',&nbsp;endurance: 'constitution', heal: 'wisdom', history: 'intelligence', insight: 'wisdom', intimidate: 'charisma', nature: 'wisdom', perception: 'wisdom', religion: 'intelligence', stealth: 'dexterity', streetwise: 'charisma', thievery: 'dexterity' }; Object.entries(skills).forEach(([skill,stat]) =&gt; { multistats[`${skill}_modifier`] = {rule: 'dnd_skill_bonus', attributes: [`${stat}_mod_level`, `${skill}-trained`, `${skill}-pen`, `${skill}-misc`]}; }); Now, I'll be honest - in this case, that's not much of a space saving, and might take more work to create than Wes's list. But there are some projects where this technique will be very valuable. I had one where I had 10 attributes, 30-ish skills, and several derived stats based on each of those attributes. I reduced what was 123 lines (each representing a sheet worker), to around 35 lines of code. A huge saving. So, there you go. A bunch of techniques, some simple, some very complex, to massively streamline making sheet workers.&nbsp;
1565028811
GiGs
Pro
Sheet Author
API Scripter
The day has caught up with me - I'll have to finish the posts above tomorrow. That said, the standard usage is all there, it's just special cases and tricks I need to add yet. So if anyone feels like trying it out, go ahead!
1565038845
Wes
Sheet Author
That is *&amp;#@^%@ Awesome!
1565067466
Wes
Sheet Author
&nbsp;&nbsp;&nbsp;&nbsp;As fate would have it I was actually getting ready to start a project tonight where I discovered I would need to add sheet workers to a sheet to fix an issue with Firefox not activating a roll button that has a disabled input being used as a label within the button element. So I was able to test your amazingly simple solution to sheet workers.&nbsp; I needed to grab some base values and create a read only attribute for an attribute backed span for the labels in roll buttons. 23 of them to be exact, that's a lot of repetition in a sheet worker and your solution gobbled it all up and spat out brilliance! Here are the multistats and multifunctions that I created: const multistats = { strength_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['strength', 'level']}, constitution_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['constitution', 'level']}, dexterity_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['dexterity', 'level']}, intelligence_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['intelligence', 'level']}, wisdom_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['wisdom', 'level']}, charisma_mod_level: {rule: 'dnd_stat_half_bonus', attributes: ['charisma', 'level']}, acrobatics_modifier: {rule: 'dnd_skill_bonus', attributes: ['dexterity_mod_level', 'acrobatics-trained', 'acrobatics-pen', 'acrobatics-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;arcana_modifier: {rule: 'dnd_skill_bonus', attributes: ['intelligence_mod_level', 'arcana-trained', 'arcana-pen', 'arcana-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;athletics_modifier: {rule: 'dnd_skill_bonus', attributes: ['strength_mod_level', 'athletics-trained', 'athletics-pen', 'athletics-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;bluff_modifier: {rule: 'dnd_skill_bonus', attributes: ['charisma_mod_level', 'bluff-trained', 'bluff-pen', 'bluff-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;diplomacy_modifier: {rule: 'dnd_skill_bonus', attributes: ['charisma_mod_level', 'diplomacy-trained', 'diplomacy-pen', 'diplomacy-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;dungeoneering_modifier: {rule: 'dnd_skill_bonus', attributes: ['wisdom_mod_level', 'dungeoneering-trained', 'dungeoneering-pen', 'dungeoneering-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;endurance_modifier: {rule: 'dnd_skill_bonus', attributes: ['constitution_mod_level', 'endurance-trained', 'endurance-pen', 'endurance-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;heal_modifier: {rule: 'dnd_skill_bonus', attributes: ['wisdom_mod_level', 'heal-trained', 'heal-pen', 'heal-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;history_modifier: {rule: 'dnd_skill_bonus', attributes: ['intelligence_mod_level', 'history-trained', 'history-pen', 'history-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;insight_modifier: {rule: 'dnd_skill_bonus', attributes: ['wisdom_mod_level', 'insight-trained', 'insight-pen', 'insight-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;intimidate_modifier: {rule: 'dnd_skill_bonus', attributes: ['charisma_mod_level', 'intimidate-trained', 'intimidate-pen', 'intimidate-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;nature_modifier: {rule: 'dnd_skill_bonus', attributes: ['wisdom_mod_level', 'nature-trained', 'nature-pen', 'nature-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;perception_modifier: {rule: 'dnd_skill_bonus', attributes: ['wisdom_mod_level', 'perception-trained', 'perception-pen', 'perception-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;religion_modifier: {rule: 'dnd_skill_bonus', attributes: ['intelligence_mod_level', 'religion-trained', 'religion-pen', 'religion-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;stealth_modifier: {rule: 'dnd_skill_bonus', attributes: ['dexterity_mod_level', 'stealth-trained', 'stealth-pen', 'stealth-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;streetwise_modifier: {rule: 'dnd_skill_bonus', attributes: ['charisma_mod_level', 'streetwise-trained', 'streetwise-pen', 'streetwise-misc']}, &nbsp;&nbsp;&nbsp;&nbsp;thievery_modifier: {rule: 'dnd_skill_bonus', attributes: ['dexterity_mod_level', 'thievery-trained', 'thievery-pen', 'thievery-misc']}, }; const multifunctions = { dnd_stat_half_bonus: function(arr) { var score = arr[0]; var half = arr[1]; var bonus = (Math.floor(score/2) -5)+Math.floor(half/2); return bonus; }, dnd_skill_bonus: function(arr) { var score = arr[0]; var trained = arr[1]; var penalty = arr[2]; var misc = arr[3]; var bonus = score + (trained*5) + penalty + misc; return bonus; }, }; I love the simplicity of what you have here GiGs! This is excellent and should help a lot of sheet authors! Wes
1565077672

Edited 1565080360
GiGs
Pro
Sheet Author
API Scripter
That's fantastic! This is exactly the kind of situation I envisioned using it. Statistics, Skill Lists, where you have a bunch of identical sheet workers, except all you change is just a couple of names.
1565096517
vÍnce
Pro
Sheet Author
Awesome contribution GiGs!
1565133788
GiGs
Pro
Sheet Author
API Scripter
Thanks, Vince. I've completed one of the placeholder posts above . Hopefully I'll have time for the last one tomorrow :)