CRP: Essence 20

Hasbro has a new house system, called Essence 20, that is used for Power Rangers, GI Joe, and Transformers, among others. The basic dice system can’t easily be handled by Roll20 dice mechanics, but I have posted befpre about howe to handle it with the normal dice mechanics: Essence 20. In this post, we’ll show you might do it with CRP. This is a surprisingly complex project.

The Dice System

First, a quick revision of how the dice system works.

Roll d20 and add another die (sometimes more than one), as follows:

  • Regular Roll: Roll d20 plus a Skill Die.
  • Edge Roll: Roll 2d20 and take the highest, then add your Skill Die.
  • Snag Roll: Roll 2d20, and take the lowest, then add your Skill Die.

The Skill Die can be d2, d4, d6, d8, d12, 2d8, or 3d6, with each treated as a single die. If you roll maximum on your die, it’s a critical (which has the weird effect that your chance of critical decreases as you get better).

Some editions of the game have the skill die as d2, d4, d6, d8, d10, d12. I’m not sure which is officially correct, but once this is done, you will be able to use either – you’ll just need to edit the single line that shows the dice range.

You can be Specialised in a skill, and in that case, you roll every die lower than your Skill up to your Skill die, and take the best. So if you have a Specialised skill at d6, you’d roll a d2, d4, and d6.

When specialised, you get a critical if any of the skill dice are criticals (except the d2). This is a really high chance (always at least 25%, and it goes up from there).

What We Need

To represent this we need several things:

  • Is it a Regular, Edge, or Snag roll?
  • Is the Skill Normal or Specialised? How do we represent this in a way that is easy to use?
  • What is the Skill Die? (d2, d4, d6, etc)
  • Optionally, the name of the roll.

Laying Out The Roll Template

Before we design the roll, we need to think about what the output should look like.

  • We’ll start with a Title bar across the top.
  • Then, a row devoted to the d20 – here we can show one or two dive, and the effective die used.
  • Then a row devoted to the Skill Die. In the case of specialised skills, this might spread over multiple rows. We’ll want to highlight the dice that is used, and also if any show a critical.
  • Finally, a row showing the result: the total of the d20 and skill die, and whether it is a critical or not. This is most important for specialised skills, where the calculation can be complex.

Maybe we’ll highlight critical rolls with a different colour, to make sure it is visible at a glance.

We don’t need to design that yet, but we do need to know how it will be structured.

Building a Roll

On the sheet, there will be a list of skills and for each skill, a button, a skill value, and whether the skill is normal or specialised. The HTML might look like this (repeated for each Skill).

<button type="action" name="act_acrobatics">
  <span>Acrobatics</span>
</button>
<select name="attr_acrobatics">
  <option selected>1d2</option>
  <option>1d4</option>
  <option>1d6</option>
  <option>1d8</option>
  <option>1d12</option>
  <option>2d8</option>
  <option>3d6</option>
</select>
<select name="attr_acrobatics_quality">
  <option value="0" selected>Normal</option>
  <option value="1">Specialised</option>
</select>Code language: HTML, XML (xml)

The Roll String

Now we need to decide the string for a typical roll. First, for a generic roll where we don’t use listed attributes but let the player enter them in the macro, we can use

&{template:essence} {{name=Essence Die roll}} {{type=[[?{Type of Roll?|Normal,1d20|Edge,2d20kh1|Snag,2d20dh1]]}} {{skill=[[?{Skill?|1d2|1d4|1d6|1d8|1d12|2d8|3d6}]]}} {{quality=[[?{Quality|Normal,0|Specialised,1}]] }}Code language: Markdown (markdown)

The string for the any given button would look more like this:

&{template:essence} {{name=Essence Die roll}} {{type=[[?{Type of Roll?|Normal,1d20|Edge,2d20kh1|Snag,2d20dh1]]}} {{skill=[[@{acrobatics}]] }} {{quality=[[@{acrobatics_quality}]] }}Code language: Markdown (markdown)

The sheet worker will have an array of the skill names, and construct an appropriate roll string for each button. For example:

const clicked = buttons => buttons.map(button => `clicked:${button}`).join(' ');

const essence_skills = ['acrobatics', 'melee', 'all other skills'];
on(clicked(essence_skills), event_info => {
   const trigger = event_info.triggerName.replace('clicked:','');
   const roll_string = `&{template:essence} {{name=${trigger} roll}} {{type=[[?{Type of Roll?|Normal,1d20|Edge,2d20kh1|Snag,2d20dh1}]]}} {{skill=[[@{${trigger}}]] }} {{quality=[[@{${trigger}_quality}]] }}`;
   startRoll(roll_string, roll => {
      console.log(roll);
      finishRoll(roll.rollId);
   });
});Code language: JavaScript (javascript)

Here we have a worker that will run when any of the skill buttons are clicked, and will send a report to the console of what exactly the roll contained. Once we have cleared up typos (I had a few!) and know it is working, we can proceed to calculating all we need to.

This is a very complex sheet worker, so as usual, I will break it up into bits for explanation, and post the complete worker at the end.

const capitalize = word => word[0].toUpperCase() + word.slice(1);
const clicked = buttons => buttons.map(button => `clicked:${button}`).join(' ');

const essence_skills = ['acrobatics', 'melee', 'all other skills'];
on(clicked(essence_skills), event_info => {
   const trigger = event_info.triggerName.replace('clicked:','');
   getAttrs([trigger, `${trigger}_quality`], values => {
      const skill_possible = ['1d2', '1d4', '1d6', '1d8', '1d12', '2d8', '3d6'];
      const die = values[trigger];
      const skill_id = skill_possible.indexOf(die);
      const quality = +values[`${trigger}_quality`] || 0;Code language: JavaScript (javascript)

We need to grab two values from the character sheet – the dice rolled (whether 1d2 or 1d12), and the quality (whether it is Normal or Specialised). The name of the action button being used helps us there.

The skill_possible array is the range of dice rolls possible. This is the one line you have to change if your system uses different dice than those listed above. Always list the number of dice – don’t use d12, use 1d12.

skill_id tells us the position in that list. d6 is the 3rd poossible die, so has an index of 2 (remember, javascript starts at 0).

      const filtered = `{${skill_possible.filter((element,index) => 
         index <= skill_id).join('cf0cs20, ')}cf0cs20}kh1`;
      const dice  = quality ? filtered : `${die}cf0cs20`;
      const roll_string=`&{template:essence} {{name=${capitalize(trigger)} roll}} {{type=[[?{Type of Roll?|Normal,1d20cf0cs0|Edge,2d20cf0cs0kh1|Snag,2d20cf0cs0dh1}]] }} {{skill=[[${dice}]] }} {{quality=[[${quality}]] }} {{total=[[0]]}}`;

      startRoll(roll_string, roll=> {Code language: JavaScript (javascript)

filtered builds a string of dice in case of Specialised quality, and dice builds the actual dice for placement in the roll string.

      startRoll(roll_string, roll=> {
         const rolls_type = roll.results.type.dice.join(', ');
         const rolls_skill = roll.results.skill.dice.join(', ');
         const total = roll.results.type.result + roll.results.skill.result;

         const calculate_max = die =>  
            Number(die.slice(0, 1)) * Number(die.slice(2));
         const critical = Number(quality ? 
            roll.results.skill.dice.some((element,index) => 
            (index > 0) && element === calculate_max(skill_possible[index])) : 
            roll.results.skill.result === calculate_max(die));Code language: JavaScript (javascript)

In the startRoll function, we start by storing the dice in their own strings. They’ll be added to computed values later so we can show them in the roll template.

We also need to calculate the total roll. Here we add two different roll template keys together (type and skill).

We end with a fiendish calculation to find out if the roll was a critical. This uses the some function, and iterates through all the dice rolled and sees if any rolled a maximum value. It takes into account that you might have multiple dice like 2d8 and 3d6 to check. It also has to report a different callculation if normal or specialised.

calculate_max is a function that takes a die like 1d6 or 2d8 and reports the maximum (6 or 16).

I encourage you to analyse that operation and see how it works. It uses a ternary operator to return a numeric 1 or 0, so we can use that in the rollTemplate.

         const type_expression = roll.results.type.expression.slice(-3);
         const type_of_roll = type_expression === 'kh1' ? 
            'Edge Roll' : type_expression === 'dh1' ? 
            'Snag Roll' : 'Normal Roll';
                
         finishRoll(roll.rollId, {
            type: rolls_type,
            skill: rolls_skill,
            total: total,
            quality: critical,
            rolltype: type_of_roll
         });Code language: JavaScript (javascript)

We also need to record if a roll is a normal roll, edge, or snag. We can get that from the type roll, which always ends with ‘kh1’, ‘dh1’, or something else. The slice function extracts text from other text – higher -3 measures from the end, and grabs the last 3 letters.

Finally, we store some of the calculated values for retrieval in the roll template.

The Roll Template

Now we have don the hard part, we can build the roll template. Here are two 3examples of dice rolls, one showing a critical and one not. You’d probably want to do more to make criticals standard out, like putting a greem border around the entire roll template.

<rolltemplate class="sheet-rolltemplate-essence">
    <div class="heading">{{name}}</div>
    <div class="results">
        <div class="key">Quality</div>
        <div class="value">{{#rollTotal() quality 0}}Normal{{/rollTotal() quality 0}}{{#rollTotal() quality 1}}Specialised{{/rollTotal() quality 1}}</div>
        <div class="key">{{computed::rolltype}}</div>
        <div class="value">{{computed::type}}</div>
        <div class="key">Skill Dice</div>
        <div class="value">{{computed::skill}}</div>
        <div class="key">Total</div>
        <div class="value">{{type}} + {{skill}} = {{computed::total}}</div>
        {{#rollTotal() computed::quality 1}}
            <div class="critical">Critical</div>
        {{/rollTotal() computed::quality 1}}
    </div>
</rolltemplate>
Code language: HTML, XML (xml)

If you’ve been following these posts, you’ve seen this layout many times. Several Logic Helpers are used to get the layout desired. See how computed properties are retrieved and inserted.

.sheet-rolltemplate-essence {
    background: white;
    border-radius: 5%;
}
.sheet-rolltemplate-essence .sheet-heading {
    background: black;
    color: white;
    text-align: center;
    line-height: 1.8em;
}
.sheet-rolltemplate-essence .sheet-results {
    display: grid;
    grid-template-columns: 49% 49%;
    column-gap: 2%;
    line-height: 1.8em;
}
.sheet-rolltemplate-essence .sheet-results .sheet-key {
    text-align: right;
}
.sheet-rolltemplate-essence .sheet-dice {
    letter-spacing: 2px;
}
.sheet-rolltemplate-essence .sheet-critical {
    grid-column: 1 / -1;
    text-align: center;
    color: white;
    background-color: black;
}Code language: CSS (css)

Again, there’s not much to say here, except the way the critical row is spread across the entire sheet using grid-column.

Concluding Words

This is a fiendishly complex worker, and this should explain why it’s hard to represent using standard roll20 dice mechanics. As promised, here is the complete worker.

const capitalize = word => word[0].toUpperCase() + word.slice(1);
const clicked = buttons => buttons.map(button => `clicked:${button}`).join(' ');

const essence_skills = ['acrobatics', 'melee', 'all other skills'];
on(clicked(essence_skills), event_info => {
   const trigger = event_info.triggerName.replace('clicked:','');
   getAttrs([trigger, `${trigger}_quality`], values => {
      const skill_possible = ['1d2', '1d4', '1d6', '1d8', '1d12', '2d8', '3d6'];
      const die = values[trigger];
      const skill_id = skill_possible.indexOf(die); // this is returning -1
      const quality = +values[`${trigger}_quality`] || 0;
      const filtered = `{${skill_possible.filter((element,index) => index <= skill_id).join('cf0cs20, ')}cf0cs20}kh1`;
      const dice  = quality ? filtered : `${die}cf0cs20`;
      const calculate_max = die =>  Number(die.slice(0, 1)) * Number(die.slice(2));
            
      const roll_string=`&{template:essence} {{name=${capitalize(trigger)} roll}} {{type=[[?{Type of Roll?|Normal,1d20cf0cs0|Edge,2d20cf0cs0kh1|Snag,2d20cf0cs0dh1}]] }} {{skill=[[${dice}]] }} {{quality=[[${quality}]] }} {{total=[[0]]}} {{rolltype=[[0]]}}`;
      startRoll(roll_string, roll=> {
         const rolls_type = roll.results.type.dice.join(', ');
         const rolls_skill = roll.results.skill.dice.join(', ');
         const total = roll.results.type.result + roll.results.skill.result;
         const critical = Number(quality ? 
            roll.results.skill.dice.some((element,index) => 
            (index > 0) && element === calculate_max(skill_possible[index])) : 
            roll.results.skill.result === calculate_max(die));
         const type_expression = roll.results.type.expression.slice(-3);
         const type_of_roll = type_expression === 'kh1' ? 
            'Edge Roll' : type_expression === 'dh1' ? 
            'Snag Roll' : 'Normal Roll';
                
         finishRoll(roll.rollId, {
            type: rolls_type,
            skill: rolls_skill,
            total: total,
            quality: critical,
            rolltype: type_of_roll
         });
      });
   });
});Code language: JavaScript (javascript)
Series Navigation<< CRP: OctaNe and InspectresCRP: Spell Slots >>

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.