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

Introducing The K-scaffold for Building Character Sheets

1643163612

Edited 1644165179
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
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.
1643178793
vÍnce
Pro
Sheet Author
WoW!  What do you do in your spare time Scott? I use vscode and simply edit the "raw" html/js and css. Not much automation in my setup other than using a few custom snippets of roll20 code, copy/paste, search/replace, etc.  Very inefficient.  Excuse my ignorance (this may be common knowledge for others...), but how is K-S handled/setup within one's coding environment? Are there extensions and/or plug-ins that help handle interpreting and compiling?  Maybe an external source on setup? Finally, this seems most beneficial for creating a new sheet, correct?
1643211600

Edited 1643350517
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Heh, most of the scaffold came out of my sheet building. Getting into something approaching a module is what took the time. As for how to setup the scaffold, I apparently completely forgot to mention how to do so; I'll need to add that to the readme! One of my goals for the future of the Scaffold is to get it released via NPM or something similar to allow it to be included like The Aaron Sheet can be, but I need to figure out how to do that since there's lots of moving parts here across several languages. In the meantime, the scaffold can be installed simply by downloading the scaffold folder located here , and putting that folder in the working directory of your project. Then just include the _kpug.pug file at the top of the pug file for your sheet and use the kscript mixin to generate your script block. I've copied my first example from the OP and bolded the relevant portions: 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 It's important that the scaffold be in a folder called scaffold  in the same directory as the main pug file for your sheet. As for who it's most beneficial for, I think new sheets would be the easiest to apply it to, but a large refactor of a sheet might be worth it to improve a sheet's maintainability, especially if it's written in raw html. However, in addition, I think that several of the JS functions would be useful to just about anyone. Take the getAllAttrs() function: const getAllAttrs = function({props=baseGet,sectionDetails=repeatingSectionDetails,callback}){ getSections(sectionDetails,(repeats,sections)=>{ getAttrs([...props,...repeats],(values)=>{ const attributes = createAttrProxy(values); orderSections(attributes,sections); const casc = expandCascade(cascades,sections,attributes); callback(attributes,sections,casc); }) }); }; Remove the three lines starting with the  createAttrProxy  call, and it becomes a pretty great generic function for getting repeating and non repeating attributes. Which would look like: const getAllAttrs = function({props=baseGet,sectionDetails=repeatingSectionDetails,callback}){ getSections(sectionDetails,(repeats,sections)=>{ getAttrs([...props,...repeats],(values)=>{ callback(values,sections); }) }); };