See the previous post | Check out the Repository | See the Next Post It's finally happening! Our sheet is going to be able to make rolls and be used in an actual game at the end of this week. Today we're talking about roll templates and custom roll parsing to power our sheet's rolls. Here's the output we'll be creating: Oh yeah, and we'll talk a little bit about character sheet dark modes! Series Posts The Beginning Do the Pug Repeating Sections and sheetworkers Style and Layout Finding our Flair! Roll on! Dimming our sheet We'll start making our roll template in a little bit, but let's talk about one of the newest features in Character sheet development; Roll20's dark mode! There are several different methods of creating a dark mode for a site. Roll20 has decided to go with injection of a class to trigger dark mode on the table top. This makes it relatively easy for us to apply dark mode to our sheets by leveraging CSS variables . If you've looked at the CSS for the sheet so far, you'll have noticed that we've been using CSS variables for quite a lot already, even though we haven't talked about them. In fact, in the last post we used them to apply our paper background as a border image. For dark mode, I've gone through and replaced just about every color definition with a CSS variable call instead. This then allows us to recolor our entire sheet with one very simple CSS declaration: .sheet-darkmode .ui-dialog,
.sheet-rolltemplate-darkmode{
--backColor:var(--dark-surface1);
--fontColor:var(--darkModeText);
--borderColor:var(--dark-primarytext);
--subHeadBackColor:var(--dark-surface1);
--selectedColor:var(--darkModeText);
--disableBackground:var(--dark-surface1);
//--shadowColor:
--disableShadow:0px 0 7px 0px #161616 inset;
--buttonColor:var(--dark-surface1);
--hoverColor:#2e2e2e;
--clickColor:black;
//Images
--paper-background:url(<a href="https://s3.amazonaws.com/files.d20.io/images/277897578/A-KBtxirzw5z2lhjc6z9Ig/original.png" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/277897578/A-KBtxirzw5z2lhjc6z9Ig/original.png</a>);
--star1Image:url(<a href="https://s3.amazonaws.com/files.d20.io/images/277891541/3QIvaKp_cOc6B8DAAXM6CA/original.png" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/277891541/3QIvaKp_cOc6B8DAAXM6CA/original.png</a>);
--star2Image:url(<a href="https://s3.amazonaws.com/files.d20.io/images/277891540/WAPK5ypyf235723n0GcVMQ/original.png" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/277891540/WAPK5ypyf235723n0GcVMQ/original.png</a>);
--star3Image:url(<a href="https://s3.amazonaws.com/files.d20.io/images/277891539/iR6cOrzXq28bpGfsqSrK0Q/original.png" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/277891539/iR6cOrzXq28bpGfsqSrK0Q/original.png</a>);
.sheetform{
background-color:inherit;
}
img{
filter: brightness(.8) contrast(1.2);
}
} And, just like that our sheet becomes dark mode enabled right? Well, not quite. We also needed to move where we define the color variables referenced here so that the dark mode class will be able to override them. This involves the two corner stones of CSS; order and specificity. Originally, our variables were defined like so: .ui-dialog .tab-content .charsheet{
/*Variables defined here*/
} But, the sheet-darkmode class is being applied to the body of the sheet, and ideally we wouldn't need to be overly specific in our declaration of the variables. The solution was to define our variables in the ui-dialog class, and then use them elsewhere as needed. This then allows us to write a concise css declaration to modify our variables for dark mode. Triggering our dark mode off of the sheet-darkmode class makes it very easy for us to apply dark mode styling, but we still need to design the color scheme for our dark mode sheet. Most of the design considerations for what color scheme to use, regardless of dark/light mode, come down to accessibility. We want to make sure that our colors can be differentiated from each other by as large a section of the population as possible. This means that we need to run our colors through some tests to make sure that they can be read easily by anyone. An easy to use tool to compare colors is WebAIM's contrast checker which gives easy to understand pass/fail grades on how two colors work together in a variety of situations. Creating a color scheme that is easy to read and stylish takes a bit of work, but the result at the end is definitely worth it. Roll those math rocks! Our sheet now styles itself to fit into the new dark mode, but let's add the last bit of big functionality to it; the ability to roll! How you design your sheet's roll template and roll functions is going to depend a great deal on what the game system's mechanics are. So, what are the dice mechanics for The Hero's Journey 2e? d20 based system attacks are compared to defense scores damage is reduced by a target's reduction value Critical damage is an optional house rule Saving throws are a flat d20 vs. a character specific target number Saves occassionally can add an attribute bonus to them Advantage/Disadvantage ala D&D 5e That's the system rules that our sheet's rolls would handle in a perfect world. We aren't going to try to automate the attack vs defense score comparison, or the damage reduction as those two things are frequently dynamic in most games. Roll Template PUG The first step in designing our roll system is going to be designing the roll template that our rolls will use. The K-scaffold provides us with several roll template related PUG mixins which we'll use to lay out our template more easily so that instead of needing to write the somewhat confusing html for a roll template: <div class="header">{{#name}}
<h3>{{name}}</h3>{{/name}}{{#character_name}}{{#character_id}}
<h4 class="character_name">[{{character_name}}](<a href="http://journal.roll20.net/character/{{character_id}})</h4>{{/character_id}}{{^character_id}}" rel="nofollow">http://journal.roll20.net/character/{{character_id}})</h4>{{/character_id}}{{^character_id}}</a>
<h4 class="character_name">{{character_name}}</h4>{{/character_id}}{{/character_name}}
</div> We can write some nice clean PUG: +templateWrapper(templateName)
.header
+templateConditionalDisplay('name')
h3 {{name}}
+characterLink The Roll20 wiki has great detail on all of the html that powers roll templates , but what it comes down to is that roll templates use named fields to insert information into the HTML, and you can check the values of these fields using roll template helper functions. The code above defines the header area for our template where the name of the roll and the character's name are going to go. In addition, we'll do conditional checks to see if some predesignated fields like roll , damage , description , and several others are present. If they are, we'll create an element to hold their contents and provide headers to easily tell what's what in our output. Here's what the roll section of our template looks like: +templateConditionalDisplay('roll')
.template-row
+templateConditionalDisplay('roll_name')
h5 {{roll_name}}
+templateConditionalDisplay('roll_name','invert')
h5(data-i18n='roll')
span.description {{roll}} So, first we check if the roll field exists, meaning that there is something other than an empty string in the field. If there is we create a template row and either display the provided roll name, or a generic "roll" header. And finally, we display the roll itself in a span. Sidebar - Using Roll templates Roll templates are part of the Roll20 chat system. The wiki explains all the ins and outs of using them, but let's do a quick review of what a message using a roll template looks like. The first part of a roll template invocation is the template call ( &{template:name} ). This tells the Roll20 chat system which template to use for injecting html into the message. Only the templates that are defined in a given game's character sheet and the default template can be used in a game. After the template call, we define fields and the content for the fields by providing a field name and the field's value like so: &{template:thj2e} {{name=Demo message}} The Roll20 backend will then take that message, find the thj2e roll template (the template we're designing right now) and provide the name field as an argument to the template which will then output the appropriate HTML which is then injected into the chat message. Roll Template Style As with everything on a character sheet, the HTML is only one piece of the roll template. It provides what elements will be created in the chat message, but now we need to style it. Just like with our character sheet, we'll use SCSS to generate the CSS for our template. Unlike our character sheet, roll templates operate under a very strict HTML and CSS sanitizer. This means that some of the fancier aspects of web development, like animations, aren't available to us. Additionally, there are some keywords which, if present anywhere in your CSS, will cause your styles to get thrown out and not used in chat. And, finally, every rolltemplate css declaration MUST have .sheet-rolltemplate- templatename (e.g. .sheet-rolltemplate-thj2e ) prepended to the declaration, and all non-Roll20 classes that are referenced in the CSS must be prepended with sheet- . These are all potential problems to keep in mind if your CSS or html doesn't seem to apply correctly. Most of the styling of roll templates is just like styling a character sheet, but there are three elements that we need to think about that we didn't in the sheet proper. One of the effects of this aggressive sanitization is that we can't define a default font-size in the html element like we did for the character sheet. This means, that unless we want to base all our sizes off of the default Roll20 font size of 10px, we'll want to define a font-size in our roll template, and then use the em unit to size our text. Roll templates can have hyperlinks in them, which means we need to style <a> tags. Additionally, ability and API command buttons are also hyperlinks themselves, so we'll want to style these specifically as well. inlinerollresults should be styled as well The SCSS for this template has avoided all those issues though, so let's take a look at some of the unique roll template styling by looking at the inline roll styling. Using SCSS' ability to nest declarations inside of each other allows us to declare the .sheet-rolltemplate-thj2e once and nest everything inside it. Additionally, we're using a wrapper div within the roll template itself to hold our content. This is to ensure that we have plenty of specificity to override the default Roll20 styles, particularly those associated with Dark Mode. The first thing that this styling does is make sure that our inline rolls are slightly larger than normal text to make sure that they pop, while still working reasonably well when used in a line of text. The SCSS declaration for the computed values ensures that when we pass a placeholder value to our template, the roll tooltip won't get displayed. This prevents useless information from showing up. Note that in order for this to work, we have to actually pass our computed rolls as [[0[computed value]]] to provide the data that the CSS needs to recognize them. And then we actually style our rolls. It may seem strange to give the same border, background-color, and padding to the four types of inline rolls, but we need to do this in order to be able to override the default Roll20 styling. And, finally, we assign some colors to our various special rolls. .sheet-rolltemplate-thj2e{
.sheet-template{
.inlinerollresult{
font-size:1.25em;
&[title*="[computed value]"],
&[original-title*="[computed value]"]{
pointer-events: none;
}
&,
&.fullcrit,
&.fullfail,
&.importantroll{
border: none;
background-color: transparent;
padding:0;
}
&.fullcrit{
color: var(--critColor);
}
&.fullfail{
color: var(--fumbleColor);
}
&.importantroll{
color: var(--importantColor);
}
}
}
} Roll Parsing Now that we've got a styled roll template, we need to actually set up the sheet to generate some rolls for us. Back when we first created the PUG for our sheet, we defined our roll buttons using a generic roll function as our listener function: +roller({name,role:'heading','aria-level':4,'data-i18n':name,trigger:{listenerFunc:'initiateRoll'}}) We'll be replacing each of those with a custom roll function appropriate to the type of roll. The types of rolls that we'll need to handle are: Saving throw or Attribute roll Attack Rolls Casting Spells The saving throw and attribute roll handler is going to be simple, and will let us take a look at how we can compute values after a roll has been made, so let's look at that code first. We've adjusted our roll button definition for these generic rolls to call the rollGeneric function to start the process of rolling. rollGeneric 's purpose is to assemble the details of the roll and then pass those details, which are contained within the rollObj , to the initiateRoll function. First we determine which of our generic rolls we are supposed to send so that we reference the correct modifier. Then we create our rollObj with keys that represent the fields we want to create, and the values that are the content that should be in those fields. Finally, we call initiateRoll which will actually send the message to chat. So, let's take a look at initiateRoll to see how we're actually using the Custom Roll Parsing feature. const rollGeneric = function(event){
const[section,id,field] = extractRollInfo(event.triggerName);
const attributeRef = attributeNames.indexOf(field) > -1 ?
`${field}_mod` :
undefined;
k.getAttrs({
props:rollGet,
callback: (attributes)=>{
const rollObj = {
name:`^{${field.replace(/_/g,' ')}}`,
roll:attributeRef ?
`[[@{roll_state} + 0@{${attributeRef}}]]` :
`[[@{roll_state}]]`
target:`[[@{saving_throw}]]`,
result:`[[0[computed value]]]`
};
//Send the roll.
initiateRoll(attributes,rollObj);
}
});
};
k.registerFuncs({rollGeneric}); initiateRoll is an asynchronous function as denoted by the async keyword right before the function definition. This then allows us to use the await keyword later so that we can pause the function's execution while startRoll resolves. Note that startRoll is the only sheetworker function that the async/await pattern can be used with. initiateRoll does three things. 1) It checks the roll type that the user has set and, if it's a query, translates the query so that the user will be prompted in their chosen language. 2) The rollObj gets parsed into the full message string and waits for startRoll to parse the constructed message and return roll results. If our roll had a result , target , and roll field that had inline rolls in them, then we also want to compute the final result of the roll (1=success; 0=failure). Based on the computed value of the result, the word failure or success will be displayed in chat. And 3) we finish the roll, which tells the chat system to actually display the result. const initiateRoll = async function(attributes,rollObj){
if(rollObj.roll){
rollObj.roll = rollObj.roll.replace(/@\{roll_state\}/,(match)=>{
if(/\?\{/.test(attributes.roll_state)){
return translateAdvantageQuery();
}else{
return attributes.roll_state;
}
});
}
const message = Object.entries(rollObj)
.reduce((text,[field,content]) => {
return text += `{{${field}=${content}}}`;
},`@{template_start}`);
const roll = await startRoll(message);
const computeObj = {};
if(roll.results.result && roll.results.target && roll.results.roll){
computeObj.result = roll.results.roll.result >= roll.results.target.result ? 1 : 0;
}
finishRoll(roll.rollId,computeObj);
}; A Functional Sheet, now what? We've got a functioning sheet that makes rolls and could be used in a game as it is right now. This post has gone a lot longer than others, but it's a complex topic that requires a lot of moving parts. I do want to hit home how Custom Roll Parsing allows us to do many things that used to require an API script or extremely complicated roll template html. We won't take a look at the code in the post, but one of the things that CRP allows us to do is to modify attribute values in reaction to a roll being made. We'll utilize this ability in our spell casting function to automatically decrement the spells available when the "cast" button is clicked. If you want to see how that looks, head on over to the repository to take a look at the full code. Next week we'll be looking at what to think about when setting up an NPC view of a sheet, and then it's just finishing touches and how to release it to the Roll20 community at large. I'm looking ahead to what sorts of blog series would be interesting in the future. If you've got questions about how something on Roll20 works, come join the Kurohyou Studios Patreon to let me know what you'd like to see next! See the previous post | Check out the Repository | See the Next Post