Hi Everyone! Character sheets have long been the most difficult piece of code to create on Roll20 because they require so many different fields of knowledge between HTML, CSS, and Javascript. Add on to that PUG and SCSS for improving the workflow of building a sheet, and building a character sheet becomes a daunting task even for experienced sheet authors. With all of that in mind, I’ve been working to build a code scaffold that could just be dropped into a new project to skip all the repetitive code that seems to make it into every project. It has since evolved from a personal tool into something that might actually be useful to others even though it's still in a beta state. I'd love to get some critique from the community and find out what features other authors want. So without further ado, I present the K-scaffold . What is it!? The scaffold provides a library of PUG mixins and javascript functions to create elements or handle tasks that I have found to be frequently used when building character sheets. It also directly connects your PUG and JS so that you can write less code overall.Additionally, the scaffold allows you to actually export information from your PUG code to your sheetworkers for use in both situations. Code Demo For this demo project, I’ve got the code for the scaffold stored in a directory called “scaffold” and my project specific javascript in a folder called "Javascript". The basics So, let’s build an extremely simple sheet demonstrating how the K-scaffold handles a common sheet problem. The problem we’ll use for the demonstration is when you have a single attribute (e.g. “strength”) that affects another attribute (“strength_mod”). Here's what the PUG code looks like for this using the K-scaffold: include scaffold/_kpug.pug
//- additional includes should be below this point
+number({name:'strength',type:'number',value:10,trigger:{affects:['strength_mod']}})
+number({name:'strength mod',type:'number',value:0,trigger:{affects:['athletics','repeating_attack_$x_mod'],calculation:'calcStrengthMod'}})
+kscript
//- All additional javascript files should start here
include Javascript/variables.js
include Javascript/demoworkers.js and Here's the generated HTML: <input name="attr_strength" type="number" value="10" title="@{strength}"/>
<input name="attr_strength_mod" type="number" value="0" title="@{strength_mod}"/>
<script type="text/worker">
//k-scaffold code here
k.sheetName = 'demo-system';
k.version = 0;/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
/*jshint -W014,-W084,-W030,-W033*/
const calcStrengthMod = function({attributes}){
return Math.floor( (attributes.strength - 10) / 2);
};
k.registerFuncs({calcStrengthMod});
</script> So, what's going on here? We're using the k-scaffold's PUG library to create two number inputs named strength and strength_mod with default values of 10 and 0 respectively along with a script tag that holds the K-scaffold's javascript functions as well as our javascript functions. The scaffold has also done a few code clean ups for us; 1) it's replaced that space in strength mod with an underscore and 2) it's added title elements to these so that our players will be able to easily see what the attribute reference for a given input is. Behind the scenes it's also done a lot more. It's hooked up our strength mod calculation function to the listener for strength mod so that any time an attribute that affects strength mod is changed, the calculation for strength mod will trigger. And the attributes object has been given a big upgrade, of which the relevant part for this demo is that it handles converting values to numbers for attributes that expect numerical values. A little more complicated Doesn't seem like the scaffold saved us that much code or made our code that much more readable right now does it? After all, all we really avoided writing was the on('change:...') section. So, let's make this slightly more complicated. Let's add in that our strength_mod affects athletics and athletics is also affected by athletics_mod . And now that we're adding several sets of inputs we should probably also add some labeling to our sheet. include scaffold/_kpug.pug
//- additional includes should be below this point
+input-label('strength',{name:'strength',type:'number',value:10,trigger:{affects:['strength_mod']}})
+input-label('strength mod',{name:'strength mod',type:'number',value:0,trigger:{affects:['athletics','repeating_attack_$x_mod'],calculation:'calcStrengthMod'}})
+input-label('athletics base',{name:'athletics base',type:'number',value:0,trigger:{affects:['athletics']}})
+input-label('athletics',{name:'athletics',type:'number',value:0,trigger:{calculation:'calcAthletics'}})
+kscript
//- All additional javascript files should start here
include Javascript/variables.js
include Javascript/demoworkers.js And the generated HTML: <label class="input-label"><span data-i18n="strength"></span>
<input class="input-label__input" name="attr_strength" type="number" value="10" title="@{strength}"/>
</label>
<label class="input-label"><span data-i18n="strength mod"></span>
<input class="input-label__input" name="attr_strength_mod" type="number" value="0" title="@{strength_mod}"/>
</label>
<label class="input-label"><span data-i18n="athletics base"></span>
<input class="input-label__input" name="attr_athletics_base" type="number" value="0" title="@{athletics_base}"/>
</label>
<label class="input-label"><span data-i18n="athletics"></span>
<input class="input-label__input" name="attr_athletics" type="number" value="0" title="@{athletics}"/>
</label>
<script type="text/worker">
//k-scaffold code here
k.sheetName = 'demo-system';
k.version = 0;/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
/*jshint -W014,-W084,-W030,-W033*/
const calcStrengthMod = function({attributes}){
return Math.floor( (attributes.strength - 10) / 2);
};
k.registerFuncs({calcStrengthMod});
const calcAthletics = function({attributes}){
return attributes.strength_mod + attributes.athletics_base;
};
k.registerFuncs({calcAthletics});
</script> Now, we're starting to get a little complicated. Many sheets handle this by calculating the value of strength_mod and then setting it to trigger the next listener. This method works, but is very slow and quickly add up to seconds of input lag on your sheet. With the K-scaffold, this is all done with a single getAttrs() and setAttrs() and the attribute setting is silent so that it doesn't cause erroneously trigger other listeners. All together this will make our sheet more responsive. Let's repeat some things Ok, but working with regular attributes is pretty easy. How about we throw in a repeating section that needs to be updated based on the value of strength_mod or a bonus attribute that is in the section itself . include scaffold/_kpug.pug
//- additional includes should be below this point
+input-label('strength',{name:'strength',type:'number',value:10,trigger:{affects:['strength_mod']}})
+input-label('strength mod',{name:'strength mod',type:'number',value:0,trigger:{affects:['athletics','repeating_attack_$x_mod'],calculation:'calcStrengthMod'}})
+input-label('athletics base',{name:'athletics base',type:'number',value:0,trigger:{affects:['athletics']}})
+input-label('athletics',{name:'athletics',type:'number',value:0,trigger:{calculation:'calcAthletics'}})
+fieldset({name:'attack'})
+text({name:'name',placeholder:'name'})
+input-label('mod',{name:'mod',readonly:'',type:'number',value:0,trigger:{calculation:'calcAttackMod'}})
+input-label('bonus',{name:'bonus',type:'number',value:0,trigger:{affects:['repeating_attack_$x_mod']}})
+kscript
//- All additional javascript files should start here
include Javascript/variables.js
include Javascript/demoworkers.js And the generated HTML: <label class="input-label"><span data-i18n="strength"></span>
<input class="input-label__input" name="attr_strength" type="number" value="10" title="@{strength}"/>
</label>
<label class="input-label"><span data-i18n="strength mod"></span>
<input class="input-label__input" name="attr_strength_mod" type="number" value="0" title="@{strength_mod}"/>
</label>
<label class="input-label"><span data-i18n="athletics base"></span>
<input class="input-label__input" name="attr_athletics_base" type="number" value="0" title="@{athletics_base}"/>
</label>
<label class="input-label"><span data-i18n="athletics"></span>
<input class="input-label__input" name="attr_athletics" type="number" value="0" title="@{athletics}"/>
</label>
<fieldset class="repeating_attack">
<input name="attr_name" placeholder="name" type="text" title="@{repeating_attack_$X_name}"/>
<label class="input-label"><span data-i18n="mod"></span>
<input class="input-label__input" name="attr_mod" readonly="" type="number" value="0" title="@{repeating_attack_$X_mod}"/>
</label>
<label class="input-label"><span data-i18n="bonus"></span>
<input class="input-label__input" name="attr_bonus" type="number" value="0" title="@{repeating_attack_$X_bonus}"/>
</label>
</fieldset>
<script type="text/worker">
//k-scaffold code here
k.sheetName = 'demo-system';
k.version = 0;/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
/*jshint -W014,-W084,-W030,-W033*/
const calcStrengthMod = function({attributes}){
return Math.floor( (attributes.strength - 10) / 2);
};
k.registerFuncs({calcStrengthMod});
const calcAthletics = function({attributes}){
return attributes.strength_mod + attributes.athletics_base;
};
k.registerFuncs({calcAthletics});
const calcAttackMod = function({trigger,attributes}){
let [section,rowID,field] = k.parseTriggerName(trigger.name);
return attributes.strength_mod + attributes[`${section}_${rowID}_bonus`];
};
k.registerFuncs({calcAttackMod});
</script> So, with the addition of a repeating section, notice a few things. The K-scaffold has automatically added the section information to the titles displaying the attribute calls (e.g. @{repeating_attack_$X_mod}). Our calculation function looks a little different with the addition of the trigger argument which holds all the details about the specific attribute that is being changed and our use of of a K-scaffold utility function, parseTriggerName , to get our section name (repeating_attack), rowID, and field name (mod) Summing a repeating section We've added an attribute to a repeating attribute, but what about the reverse? GiGs built the beautiful repeatingSum function to help folks do this. But, how would we do something like repeatingSum in the K-scaffold? include scaffold/_kpug.pug
//- additional includes should be below this point
+input-label('strength',{name:'strength',type:'number',value:10,trigger:{affects:['strength_mod']}})
+input-label('strength mod',{name:'strength mod',type:'number',value:0,trigger:{affects:['athletics','repeating_attack_$x_mod'],calculation:'calcStrengthMod'}})
+input-label('athletics base',{name:'athletics base',type:'number',value:0,trigger:{affects:['athletics']}})
+input-label('athletics',{name:'athletics',type:'number',value:0,trigger:{calculation:'calcAthletics'}})
+fieldset({name:'attack'})
+text({name:'name',placeholder:'name'})
+input-label('mod',{name:'mod',readonly:'',type:'number',value:0,trigger:{calculation:'calcAttackMod'}})
+input-label('bonus',{name:'bonus',type:'number',value:0,trigger:{affects:['repeating_attack_$x_mod']}})
+input-label('weight',{name:'weight',type:'number',value:0,trigger:{affects:['weapon_weight']}})
+input-label('quantity',{name:'quantity',type:'number',value:0,trigger:{affects:['weapon_weight']}})
+input-label('weapon weight',{name:'weapon weight',type:'number',readonly:'',value:0,trigger:{calculation:'calcWeaponWeight'}})
+kscript
//- All additional javascript files should start here
include Javascript/variables.js
include Javascript/demoworkers.js And what our calculation function looks like: const calcWeaponWeight = function({trigger,attributes,sections}){
return sections.repeating_attack.reduce((total,rowID)=>{
return total +
attributes[`repeating_attack_${rowID}_weight`] *
attributes[`repeating_attack_${rowID}_quantity`];
},0);
};
k.registerFuncs({calcWeaponWeight}); That sections argument is an object that holds arrays with all the IDs of the rows in the sections. As an added bonus, they're even ordered the same way they are in the repeating section! So, we can just iterate through the IDs for the repeating_attack section and total up our weapon weights that way. Forward the K-Scaffold! This demo sheet is just a small sampling of what the K-Scaffold can currently do. The attributes object has been upgraded so that setting and working with attribute values is easier and more straightforward. The PUG mixins allow for exporting data to the script block so that you can use the same array or object to build a section of code as you do to iterate over it in your javascript. However, the K-scaffold is by no means complete. There are quite a few things I'd like to add or improve about the scaffold including: Add SCSS default stylings and clearing of Roll20 styles from elements track attribute changes - This would allow logging a given attribute to get a complete history of how it has been mutated since the last element change. translation.json generation - Not sure this is possible, but a more streamlined generation of translation files would be wonderful The documentation on the scaffold will hopefully be beneficial to folks, but let me know if anything isn't clear (or is missing) as I'm sure it could be better written. Let me know what you're excited to do with scaffold, or even what you want to cannibalize from it! And, if you want to support the development you can find me on patreon ! Interested in learning how to build a character sheet? Check out my forum post series A Sheet Author's Journey for a guided walkthrough of building an actual sheet from scratch.