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

A Sheet Author's Journey - Repeating sections and writing sheetworkers!

1645044958

Edited 1648660166
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Welcome back to our journey into creating a sheet from scratch! Last time we created the non repeating parts of the PC character sheet for  The Hero's Journey.  This included setting up the triggers for several of our attributes so that they would be calculated by sheetworkers we were going to write in the future. Well, the future is now, and it's time to write our repeating sections and dive into writing our sheetworkers. By the end of today's code write, we'll have a functional sheet, although it's going to look UGLY as we'll be writing CSS next week. As always the complete code that was written in the last week can be found in the  tutorial's github repo . The code in the repo has in depth commenting to explain what's going on line by line. So, our first task today is to get our repeating sections created: Series Posts The Beginning Do the Pug Repeating Sections and sheetworkers Style and Layout Finding our Flair! Roll on! Repeating sections Repeating sections are areas of a character sheet that can have any number of repeating elements within them that will all share the same data structure and styling. In raw HTML, we create these by defining a fieldset element with a class of repeating_ sectionname . It's important that this section name not have any underscores in the name. Of nearly equal importance is that we not add any additional classes to this fieldset; doing so will seem to work just fine until you try to call an action button that is present in the section from chat. The K-scaffold will handle formatting out a lot of these errors for us, but they're still good to keep in mind. At the beginning of this tutorial series, we created a (very basic and ugly) sketch of our layout goal. The bottom half of that layout was going to be populated by several repeating sections; weapons, armor, lineage and archetype abilities, equipment, and spells. For our first repeating section, I'm actually going to skip the order a little and go over the lineage abilities section because it is going to be one of the most basic sections. Here's the K-scaffold PUG code for that section: section#lineage.lineage.paper-background h4(data-i18n='lineage abilities') +fieldset({name:'lineage'}) +collapse() +text({name:'name',class:'underlined'}) .headed-textarea.notes.expanded +h5({'data-i18n':'notes'}) +adaptiveTextarea({name:'notes',class:'underlined'}) That's pretty simple, with only 8 lines of code. We're defining a section of our sheet with the html section  element, and populating it with a header and a fieldset. Note that we're using the fieldset  mixin from the K-scaffold. This is going to do several things for us. We just need to specify the name of the section as a simple name without the repeating_  prefix. The fieldset mixin will take care of adding the repeating field details to the title property of each of the elements in the fieldset The fieldset mixin will take care of hooking up our javascript listeners to the repeating section appropriately. We'll talk more about this in the second half of today's tutorial. Inside our fieldset, we've defined several things. First of all is a collapse mixin that I've made in our sheet specific mixins. This will generate a checkbox that we'll be able to style off of in our CSS to hide/show details in each row. The mixin's code looks like: mixin collapse(name='collapse') +checkbox({name,value:1,class:'collapse'}) //- End Mixin Pretty simple, but making it a mixin allows us to avoid retyping all those checkbox details every time we need to use one. This will also help to limit bugs in our code because there's less chance for a typo to slip in. And then, we've just got a text input for the name of the ability and a textarea for the description. As you may have noticed, other than being nested in a fieldset, the code for these attributes looks pretty much identical to the code for our non repeating attributes. This makes writing fieldset both very easy, and occassionally frustrating. The easy part is obviously that we don't have to learn an entire new syntax to generate our sections. The frustrating part is that for several pieces of how Roll20 character sheets function, we can't reuse attribute names in non-repeating and repeating attributes. This issue actually bit us in this very project. Let's see how by looking at a more complex repeating section, repeating_armor : section#armor.armor.paper-background +input-label({ label:'attack modifier', inputObj:{name:'attack modifier',type:'number',class:'underlined',trigger:{affects:['repeating_weapon_$X_attack']}} }) .repeat-columns h4(data-i18n='armor and shield') each label in ['reduction value','defense bonus','traits','aspects'] h5(data-i18n=label) +fieldset({name:'armor'}) +collapse() +checkbox({name:'equipped',value:1,trigger:{affects:['defense_total','reduction_value']}}) +text({name:'name',class:'underlined'}) +number({name:'reduction',class:'underlined',trigger:{affects:['reduction_total']}}) +number({name:'defense',class:'underlined',trigger:{affects:['defense_total']}}) each name in ['traits','aspects'] +text({name,class:'underlined'}) .headed-textarea.notes.expanded +h5({'data-i18n':'notes'}) +adaptiveTextarea({name:'notes',class:'underlined'}) Ok, so we've got more code in this section. The code before the fieldset is defining a series of column labels that will specify what each column in our repeating section is for so that players can know at a glance what the stats of their weapon(s) are. These won't make much sense when we look at our sheet in the Roll20 sandbox until we write our CSS next week. The first two lines of our repeating section are also pretty familiar as our fieldset declaration and our collapse mixin call. In our third line, we're defining another checkbox that will be used to mark a piece of armor or a shield as being equipped, and we've set it up so that changes to it will affect a few attributes, which are contained in the array: ['defense_total','reduction_value'] You can find the reduction_value  attribute in the _defenses.pug file from the full code upload from last week's post, but that same file doesn't have defense_total , instead it has an attribute called defense . You might notice that in our repeating_armor  code above, we also have a defense  attribute that is inside our repeating section. Having repeating and non-repeating attributes like this can cause issues with the sheetworker listeners, and so should be avoided. In this case, I renamed the non-repeating version to defense_total  because it's going to store the total defense of the character. This repeating section is going to be affecting two non-repeating attributes, but you can take a look at the _weapons.pug section file in today's uploaded code to see what this looks like for repeating attributes that are being affected by other attributes. So, we've got several of our repeating sections created. I'll be writing the spell code before next week's post so that we can style the entire sheet then. Since we've got (almost) our entire sheet created, now we can start writing our javascript to actually power the sheet. Sheetworkers - Javascript by any other name The Roll20 wiki has excellent information about the basics of sheetworkers. Because we're using the K-scaffold, we actually won't be writing any code using sheetworker functions today. For the attribute calculations that we're writing, the K-scaffold is going to handle getting the IDs of the rows created in each repeating section, getting the values of our attributes, and even setting the attributes that need to be changed. To understand how it's doing this, let's take a look at the PUG definitions of a few of the attributes that we've created, specifically the reduction_value  and reduction_mod  attributes from the defenses section. Note that in last week's post we made reduction_mod  as reduction_value_bonus , but I've renamed it this week so that we can write a better calculation function that will work for both our defense_total  and reduction_value  attributes. +number({name:'reduction value',readonly:'',class:'underlined',trigger:{calculation:'calcDefense'}}) +number({name:'reduction mod',class:'underlined',trigger:{affects:['reduction_value']}}) The first parts of these two mixin calls are pretty straightforward definitions of html properties to apply to the element. But, at the end of the mixin, we've got a trigger property that has an object as it's value. trigger  isn't an html property at all; it's actually a K-scaffold property that tells the scaffold what to do with this attribute. If a trigger is not defined for an attribute, the K-scaffold will add it to the list of attributes to get, but won't create a listener for it. If the attribute does have a trigger, then a listener gets created and what happens when that listener is triggered depends on what was specified in the attribute's trigger. For our reduction_value  attribute, we're telling the scaffold that a calculation function should be called whenever an attribute that affects reduction_value  is changed. For reduction_bonus , we're telling the scaffold that changes to reduction_bonus  also affect one or more other attributes (just reduction_value  in this case). Setting up connections like this has several benefits over writing the raw html and creating the listeners ourselves: We can simplify our javascript coding because we only ever need to think about how to return the value of an individual attribute, not how to handle the whole cascade of attributes that might be affected by a single change. Making edits to how the sheet works is also easier. We can connect an attribute into a cascade simply by editing the affects property of the attribute to added and doing some simple edits to the calculation function. Removing an attribute works similarly. This limits the number of locations that we might need to make edits while maintaining our sheet after it launches and makes it easier to find bugs in our sheet: There are now only two place where the calculation of our attribute's value might go wrong. Either the calculation function has a bug in it, or an attribute is not set to affect the right attribute. Here's what the calculation function for our reduction_value and defense_total  attributes looks like: const calcDefense = function({attributes,sections,trigger}){ let name = trigger.name.replace(/_.+/,''); let activeArmorIDs = sections.repeating_armor.filter((id)=>attributes[`repeating_armor_${id}_equipped`]); let finesse = name === 'defense' ? 10 + attributes.finesse_mod : 0; let activeArmorBonus = activeArmorIDs.reduce((total,id)=>total += attributes[`repeating_armor_${id}_${name}`],0); return attributes[`${name}_mod`] + finesse + activeArmorBonus; }; k.registerFuncs({calcDefense});//Register the function So, we've got 3 lines of setup and logic for the function to figure out what to do. It extracts a name variable from our trigger's name property to find out which attributes to affect, sorts through our armor rows to figure out which ones are currently equipped, and then figures out if we're calculating defense_total  or not to add the finesse modifier. The final line of the function simply returns the total of all of these factors to the K-scaffold which will take care of setting them on the sheet for us. This function could have been much simpler if we had decided to write a unique function to calculate  reduction_value  and defense_total , but that would have created additional code that we'd have to maintain. Doing it this way adds a couple lines of complexity to our function, but limits the number of functions we have to maintain. There's no real right position on this spectrum of function complexity and number of functions; what matters is what works for you. And the final line of code here simply registers the function to the k-scaffold so that the k-scaffold can call it when needed. So, let's actually test this code out and see what it looks like. When we first upload this code, here's what our console log will look something like this: Because we haven't set the version of the sheet, the K-scaffold's debug function is on and it will give us a reasonably verbose summary of what is happening as it processes events. You may notice that there are several !!!Warning!!!  messages there. These are for javascript functions that we have not written yet, but that we referenced in our pug file. The K-scaffold will let you know if you are trying to hook into a function that does not exist. At the end though, our sheet is loaded, and we can see our calcDefense  function listed in the function keys that the K-scaffold did find. Let's make a character and see what our sheet looks like: Thanks to Keith Curtis for extracting the system logo for me. Other than that awesome looking logo, this sheet doesn't look great, but we're going to fix that next week when we start writing our CSS and upload our translation file. How about our sheetworker functions? Let's see if they work: Yep! Those work great! What's next? We've got a nicely functional sheet taking shape, and we'll start making it look as good as it runs next week! We'll be talking about translation files, SCSS, CSS, and design principles. What functionalities are you all working on bringing to your sheets? What challenges are you encountering? And, hit me up with questions! If you're enjoying this series of posts, d rop by the   Kurohyou Studios Patreon ! See the previous post  |  Check out the Repository  | See the next post
1645573183
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Week four is up where we talk about sheet layout, CSS, and SCSS!
Great Post, thank you for providing good resources to develop better custom sheet. I've successfully followed along up to the forth post, however I'm currently stuck, trying to make sense of the triggers options. I was wondering if someone could help me out? I have a basic attribute section for now, really simple where I have a button to trigger the roll for the attribute.  I have set the trigger for the attribute as follow, following what I've seen in the tutorial: PUG section: - varObjects . attributeNames = attributes ; section #attributes.sheet-attributes h5 .score-head Score h5 .modifier-head Modifier each name in attributes - const label = name [ 0 ]. toUpperCase () + name . substring ( 1 ); div .justified-content + roller ({ name , role: 'heading' , 'aria-level' : 4 , trigger: { listenerFunc: 'initiateRoll' }}) #{ ` ${ label } ` } + number ({ name , class: 'underlined' , value: 0 , trigger: { affects: [ ` ${ name } _mod` ]}}) + number ({ name: ` ${ name } _mod` , readonly: '' , value: 0 , class: 'underlined' , trigger: { calculation: 'calcAttrPercentage' }}) span ( class = "absolute-percent" ) % Fow now I was just trying to log something in the initiateRoll function in order to confirm that the function was indeed called, but so far I haven't been able to make it work: Javascript: const calcAttrPercentage = function ({ attributes , trigger }) { let attribute = trigger . name . replace ( /_mod/ , "" ); return attributes [ attribute ] * 5 ; }; k . registerFuncs ({ calcAttrPercentage }); //Register the function const initiateRoll = function ({ attribute , trigger }) { console . log ( "trigger" , trigger ); }; k . registerFuncs ({ initiateRoll }); //Register the function Nothing gets logged unfortunately and I still haven't figure out why, If someone could help me out, I would be grateful. If someone needs more context regarding this issue, I would be happy to provide more information. Nevertheless, the library seems very promising, Nice work Scott.
1647284136

Edited 1647284153
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Thanks to Sin for finding this issue. This is a bug in the current iteration of the K-scaffold. The scaffold is missing a function that I had been writing in each of my sheets, but forgot to add to the K-scaffold code base. I'm working on an update to the scaffold that will fix this bug and several others. Until I get the scaffold udpated, add the following function to your JS and register it to the k-scaffold: const setActionCalls = function({attributes,sections}){ actionAttributes.forEach((base)=>{ let [section,,field] = k.parseTriggerName(base); let fieldAction = field.replace(/_/g,'-'); if(section){ sections[section].forEach((id)=>{ attributes[`${section}_${id}_${field}`] = `%{${attributes.character_name}|${section}_${id}_${fieldAction}}`; }); }else{ attributes[`${field}`] = `%{${attributes.character_name}|${fieldAction}}`; } }); }; k.registerFuncs({setActionCalls}); This will be called by the K-scaffold when the sheet is opened, and when the character_name changes to setup the backend roller attributes.
1647364197
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Thanks again to Sin for pointing out the missing functionality in the K-scaffold. I've updated the primary K-scaffold directory in the repo with updated code that fixes this bug and adjusts how some mixins are used. I'll be updating the example code through out the blog series over the course of the week.