Wounds in the Carrington Sheet

Characters have a number of boxes of stress that can be marked, with the number directly based on their Courage ability.

Characters also have several health conditions which each need a description. Most characters have the same number, but this can be altered with knacks.

Then there are special checkboxes: Sacrifice, Concession, and Manifestation. Once checked, these can’t be used again that Chapter. Manifestation is special – it is tied into the Eldritch Powers system, and is only available when your Eldritch Power can increase.

This is easily the most complex of these sections. We’ll look at each part.

Stress

The basic idea is simple. When characters lose a conflict roll, they suffer from 1-3 Stress. They have a Stress attribute, usually ranging from 2-5, and can mark their damage here. Roll20 has a damage state system, where each attribute has both a current rating and a max rating. This is ideal for adding attributes to the token bar. So, we set the max equal to current Stress, and reduce the current score for each point suffered.

Since it’s a small total, it’s reasonable to have one visible checkbox for each point of stress, and players might mark their damage here – and the sheet needs to count how many boxes are marked and assign that to Stress.

When the stress button is clicked, the player is prompted for how much stress to take, and then checkboxes are ticked. If a character takes more stress than they have remaining, the overflow is reported.

We need a lot of sheet workers to make all this work – it turns out to have some complexity you might not expect.

Layout

First, let’s create and style the layout.

<div class="stress">
  <input type="hidden" name="attr_Stress" value="1">
  <input type="hidden" name="attr_Stress_max" value="1">
  <button type="action" name="act_stress">
     <span>Stress</span>
  </button>
  {{#repeat count=7 start=1}}
  <input type="hidden" name="attr_stress{{@index}}_hide" class="stress-hide"
         value="{{#if @first}}1{{else}}0{{/if}}">
  <input type="checkbox" name="attr_stress{{@index}}" value="1" class="stress-box">
  {{/repeat}}
</div>Code language: Handlebars (handlebars)

This code block uses Handlebars to create the HTML, but there are only two places it matters – the repeat block, where two inputs are duplicated 7 times, inside the value of one of those inputs, where it’s checked if its the very first item in the repeat.

The first input (stress-hide) is used to set whether the next input is visible or hidden. Recall that there are 7 sets of inputs, one for each index of Courage from Poor to Legendary. Since the default value of Courage for new characters is Poor, that first input should be set to 1, and the rest 0. That’s what the if statement does.

Styling the Section

The way the input boxes are shown or hidden is passed on a by now very familiar technique:

div.stress .stress-hide:not([value="1"]) + .stress-box {
    display: none;
}Code language: CSS (css)

The layout of the stress section and the spacing of the checkboxes is handled with straightforward CSS:

.health .stress {
    display: grid;
    grid-template-columns: 45px repeat(7, 20px);
    column-gap: 2px;
    align-content: start;
    margin-bottom: 5px;
}
.health .stress input[type="checkbox"] {
    height: 20px;
    width: 20px;
    margin-top: 3px;
}
.charsheet .health .stress button {
    height: 20px;
}Code language: CSS (css)

There’s some code there to size the stress button. Some more code will be added later, but we’ll come back to that later. For now, we have three sheet workers to create!

Sheet Worker For Courage Changes

When Courage changes, the maximum value of Stress changes and along with that, the visible checkboxes also change.

on('change:courage', () => {
   getAttrs(['courage'], v => {
      const courage = v.courage;
      const index = ranks.indexOf(courage);
      const output = {};
      output.stress_max = index;
      output.stress = 0;
      seq(7, 1).forEach(i => {
         output[`stress${i}_hide`] = i <= index ? 1 : 0;
         output[`stress${i}`] = 0;
      });
      setAttrs(output, {silent:true});
   });
});Code language: JavaScript (javascript)

Sheet Worker For Handling Stress Changes

Stress can be changed in three ways: clicking the visible checkboxes to add or subtract stress, adding or subtracting to the stress score directly, or clicking ther Stress button. That last one has its own sheet worker. This worker handles the first two situations, but since each method is different, a check is made using event_info to figure out which one to follow.

Also note that when a stress box is changed, the stress score changes, which would trigger this sheet worker again. Or when the stress sore changes, the boxes would be changed, wich again triggers the sheet worker. To stop this, the setAttrs function has the {silent:true} instruction – that means this sheet worker can’t trigger new sheet workers. It can’t trigger itself.

on(`change:stress ${seq(7,1).map(i => `change:stress${i}`).join(' ')}`, (event_info) => {
   getAttrs(['stress', ...seq(7,1).map(i => `stress${i}`)], v => {
      const button = (event_info.sourceAttribute === 'stress'|| event_info.triggerName === 'stress')  ? 1 : 0;
      const output = {};
      if(button) {
         const current = int(v.stress);
         seq(7,1).forEach(i => {
            output[`stress${i}`] = i <= current ? 1 : 0;
         });
      } else {
         let current = 0;
         seq(7,1).forEach(i => {
            current += int(v[`stress${i}`]);
         });
         seq(7,1).forEach(i => {
            output[`stress${i}`] = i <= current ? 1 : 0;
         });
         output.stress = current;
      }
      setAttrs(output,{silent:true});
   });
});Code language: JavaScript (javascript)

The Inflict Stress Button

When the stress button is clicked, the player is prompted for how much stress to add to the character. That stress is added (triggering the previous sheet worker), and a message is sent to chat declaring how much stress has been suffered. If the character is out of Stress, and more damage remains, that is also reported.

{{{{noop}}}}
on('clicked:stress', () => {
   getAttrs(['stress', 'stress_max'], v => {
      const current = int(v.stress);
      const max = int(v.stress_max);
      const output = {};
      startRoll("!{template:default}{{ask=![[?{How much Stress to add?|None,0|One,1|Two,2|Three,3}]]}}", (question) => {  
         finishRoll(question.rollId);
         const query = question.results.ask.result;
         let report_string = `@{character_name} gains ${query} stress`;
         if((current + query) > max) {
            const overflow = (current + query) - max;
            report_string += `, with ${overflow} overflow`;
         };
         output.stress = Math.min(max, current + query);
         startRoll(`&{template:default}{{name=${report_string} }}`, (report) => {  
            setAttrs(output);
            finishRoll(report.rollId);
         });
      });
   });
});
{{{{/noop}}}}Code language: JavaScript (javascript)

The sheet worker is nested inside a special {{{{noop}}}} block. this is purely for handlebars users. The startRoll commands include a rollTemplate, and handlebars doesn’t cope well with them (it’s the double cully brackets {{ }}). The {{{{noops}}} block means handlebars doesn’t process this worker, and all is saved.

The worker is a Custom Roll Parsing block, and includes finishRoll twice. This function does nothing here, but must be included whenever you have startRoll – otherwise the roll has a significant delay as Roll20 waits for it.

Conditions

Note: at the time this was written, characters had two visible Conditions; now they have three. It’s a good idea to sort your system out before creating the character sheet!

Conditions are much simpler, but do have some very tricky bits. Here’s the HTML.

<div class="conditions">
   <div class="header">
      <h4>Label</h4>
      <h4>Ability</h4>
      <h4 class="center">X</h4>
   </div>
   {{#each condition}}
   <input type="hidden" name="attr_condition{{@index}}_hide" 
          class="condition-hide" value="1">
   <details class="condition">
      <summary>
         <span>{{this}}</span>
         <select name="attr_condition{{@index}}_ability">
            <option selected>None</option>
            {{#each @root.abilities}}
            <option>{{this}}</option>
            {{/each}}
            <option>Custom</option>
         </select>
         <input type="checkbox" class="standard" 
                name="attr_condition{{@index}}_check" value="1">
      </summary>
      <textarea name="attr_condition{{@index}}_details"></textarea>
   </details>    
   {{/each}}
</div>Code language: Handlebars (handlebars)

There are several things to notice here. First, the each block is a handlebars construction: it just means the code inside that block is duplicated for each Condition. The four possible conditions are Bruised, Bloody, Battered, and Broken. A complication is that the first and last are normally invisible – characters start with the Bloodied and Battered conditions, but not Bruised or Broken. condition-hide checkboxes define whether the entire details block is visible or not – that will be set later in a sheet worker.

Each Condition also has a dropdown showing all Abilities, as well as None and Custom – when you suffer a condition you must choose which ability is most affected, and by checking None, the Condition is unchecked.

Styling The Condition Block

The conditions section has a lot of styling, much of which is similar to styling already shown in the sheet. One significant part is that the header div and details summary share the same grid styling, to make sure their columns line up properly.

div.conditions .condition-hide:not([value="1"]) + .condition {
    display: none;
}
.conditions details summary,
.conditions .header {
    display: grid;
    grid-template-columns: 55px 1fr 30px;
    column-gap: 5px;
    align-content: start
}
.conditions h4 {
    text-decoration: underline;
    font-size: 13px;
}
.conditions select {
    width: 100%;
    font-size: 0.9em;
    margin-bottom: 0;
}
.conditions textarea {
    width: calc(100% - 10px);
    border-radius: 10px;
    height: 65px;
    font-size: 0.9em;
}
.conditions span {
    font-weight: bold;
    margin-top: 5px;
}Code language: CSS (css)

When a Condition is Checked

When a character suffers a Condition, they choose which Ability to be affected. This sheet worker responds to that Ability select: when you pick one, that check box is ticked, and when you pick None, it is unchecked.

{{{{noop}}}}
on(seq(4).map(i => `change:condition${i}_ability`).join(' '), (event_info) => {
   getAttrs(seq(4).map(i => `condition${i}_ability`), v => {
      const output = {};
      seq(4).forEach(i => {
         const check = v[`condition${i}_ability`];
         output[`condition${i}_check`] = check === 'None' ? 0 : 1;
      });
      const ability = event_info.newValue;
      if(ability != 'None') {
         const report_string = 
            `@{character_name} suffers a Condition and ${ability} is impaired.`;
         startRoll(`&{template:default}{{name=${report_string} }}`, (report) => {  
            finishRoll(report.rollId);
         });
      }
      setAttrs(output);
   });
});
{{{{/noop}}}}Code language: JavaScript (javascript)

This worker uses the trusty seq() function again in several places- that’s quite a workhorse for this sheet. There are four conditions, so seq(4) is used a lot.

The worker reports in chat when a new condition is suffered and an Ability is impaired. Because the rollTemplate is here, the entire worker is enclosed in an {{{{noops}}}} block.

One-Use Triggers

There are two checkboxes and a Manifest button. We’ll describe the latter in the next section – for now, let’s look at the Sacrifice and Concession checkboxes.

<div class="one-use">
   <div class="triggers">
      <span class="title">Sacrifice</span>
      <input type="checkbox" name="attr_sacrifice" value="1">
      <span class="title">Concession</span>
      <input type="checkbox" name="attr_concession" value="1">
   </div>
   <div class="manifest">
      <button type="action" name="act_manifest">
         <span class="title">Manifest a Power</span>
      </button>
   </div>
</div>Code language: HTML, XML (xml)

Styling the One-Use Section

Laying out this section is really simple – just a grid inside another grid, to the get the different items position where I want them. Other styling, like title is already done.

.health .one-use {
    display: grid;
    column-gap: 5px;
    grid-template-columns: 110px 1fr;
    margin-top: 5px;    
}
.health .one-use .triggers {
    display: grid;
    column-gap: 5px;
    grid-template-columns: 1fr 30px;
}
.health .one-use .manifest button {
    margin: 10px;
    margin-top: 5px;
    margin-left: 5px;
    height: calc(100% - 20px);
    width: calc(100% - 20px);
}Code language: CSS (css)

Announcing Their Use

When the checkboxes for Sacrifice or Concession are checked, a message is sent to chat announcing this. This code uses a technique I called Universal Sheet Workers, which you can find on the wiki. Given an array of variables, you can construct multiple sheet workers. Since these two sheet workers are identical except for their names, they are perfect for this technique.

{{{{noop}}}}
['Sacrifice', 'Concession'].forEach(w => {
   on(`change:${w.toLowerCase()}`, ()=> {
      getAttrs([w], v => {
         const check = int(v[w]);
         if(check) {
            const report_string = `@{character_name} uses a ${w}.`;
            startRoll(`&{template:default}{{name=${report_string} }}`, (report) => {  
               finishRoll(report.rollId);
            });
         }
      });
   });
});
{{{{/noop}}}}Code language: JavaScript (javascript)

Manifesting a Power

Use of this button involves clearing and refreshing used powers and among other things. These are handled in detail in the Sidebar, so this button’s click event will be dealt with in the Sidebar post.

But the visibility of this button depends on things more pertinent to powers, so that can be handled here. In brief, this button is visible and clickable when two conditions are true:

  • Your Eldritch Power rank is below your main experience rank (character_rank)
  • And you have not used any Eldritch powers this advance (set a hidden value to 1 whenever a power is used)

We need a sheet worker to create a number with a value of 0 or 1, based on these conditions, and check that value in CSS to see this button should be visible or not.

This needs two inputs, the first can be placed anywhere, the second must be just before the div.manifest, like so:

<input type="hidden" name="attr_powers_used" class="powers_used" value="0" >
<input type="hidden" name="attr_manifest_allowed" class="can-manifest" value="0" >
<div class="manifest">Code language: HTML, XML (xml)

Manifest CSS

Here is a very familiar style of CSS.

div.one-use .can-manifest:not([value="1"]) + .manifest {
    display: none;
}Code language: CSS (css)

Manifest Sheet Workers

And finally, the sheet worker to calculate the input that the CSS uses.

on('change:powers_used change:eldritch change:character_rank', () => {
   getAttrs(['powers_used', 'eldritch', 'character_rank'], v => {
      const powers = int(v.powers_used);
      const e = v.eldritch;
      const c = v.character_rank;
      const terrible = +(e === "Terrible");
      const difference = ranks.indexOf(e) >= ranks.indexOf(c);
      const not_allowed = Math.max(difference, powers, terrible);
      setAttrs({
         manifest_hidden: 1 - not_allowed
      });
   });
});Code language: JavaScript (javascript)

This also checks if Eldritch rank is Terrible. At that rank, you can never gain any powers, so the Manifest button is never visible.

This could be written more concisely, but it’s written the way it is to allow console.log statements to interrogate values to make sure everything is working properly. And I needed that – when you start fiddling with true or false values, it can get complicated.

Series Navigation<< Eldritch Powers in the Carrington SheetRolls and the Rolltemplate in the Carrington Sheet >>

Leave a Reply

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