How Many Abilities in the Carrington Sheet

This section of the character sheet is more complex than it loks – the deep green part of the picture here.

There are several sections:

  • at the top, you can see the Advances (Poor, Fair, etc), which show character advancement.
  • Then the How Many Abilities section shows how many abilities you have at each rank, and how many you should have.
    If you don’t have the correct numbers, they are red. You can adjust the Abilities part of the sheet, which is visible, to correct this.
  • Then you have a box containing several buttons and the XP total. These buttons are described below.
  • Finally you have text telling you how many Extras you now have – you can select these elsewhere, but if you have the incorrect amount this will be red.

How Many Abilities

Heroes have a fixed number of Ability ranks,and the number is fixed with experience. That means that when you say how advanced a character is, you know exactly how many Abilities at each rank they should have.

So we build a section which has a row foir each Rank, and in that row has three elements – hidden input we’ll come back to, and an inoput to show how many Abilities the character owns of that rank, and another for how many they should have. Here’s the HTML for that:

<h4>How Many Abilities</h4>
<div class="abilities">
   <span class="title">Rank</span>
   <span class="title center">Owned</span>
   <span class="title center">Needed</span>
                        
   <div class="show-through">
      <span>Poor</span>
      <input type="hidden" name="attr_poor_check" class="check" value="0">
      <span name="attr_poor_owned" class="red">0</span>
      <span name="attr_poor_needed" class="red">0</span>
   </div>                    
                        
   <div class="show-through">
      <span>Fair</span>
      <input type="hidden" name="attr_fair_check" class="check" value="0">
      <span name="attr_fair_owned" class="red">0</span>
      <span name="attr_fair_needed" class="red">0</span>
   </div>                    
                        
   <div class="show-through">
      <span>Good</span>
      <input type="hidden" name="attr_good_check" class="check" value="0">
      <span name="attr_good_owned" class="red">0</span>
      <span name="attr_good_needed" class="red">0</span>
   </div>                    
                        
   <div class="show-through">
      <span>Great</span>
      <input type="hidden" name="attr_great_check" class="check" value="0">
      <span name="attr_great_owned" class="red">0</span>
      <span name="attr_great_needed" class="red">0</span>
   </div>                    
                        
   <div class="show-through">
      <span>Superb</span>
      <input type="hidden" name="attr_superb_check" class="check" value="0">
      <span name="attr_superb_owned" class="red">0</span>
      <span name="attr_superb_needed" class="red">0</span>
   </div>                    
                        
   <div class="show-through">
      <span>Spectacular</span>
      <input type="hidden" name="attr_spectacular_check" class="check" value="0">
      <span name="attr_spectacular_owned" class="red">0</span>
      <span name="attr_spectacular_needed" class="red">0</span>
   </div>                    
                        
</div>Code language: HTML, XML (xml)

It is divided into divs for convenience, one div for each rank. They are given the class show-through which has display:contents. This means that the div is ignored when calculating the CSS Grid, so the columns created for that still apply.

Finally, the hidden input is used for CSS. In a sheet worker the amount of the abilities owned and needed are calculated, and when they differ the hidden input is given a value of 1, otherwise 0. Then a CSS rule is checked – if that value equals zero, those two spans are coloured red.

Notice there is a lot of near-duplication there. Each div is very similar to each other. So, I use Handlebars to construct that code with a much smaller code block.

<h4>How Many Abilities</h4>
<div class="abilities">
   <span class="title">Rank</span>
   <span class="title center">Owned</span>
   <span class="title center">Needed</span>
   {{#each ladder}}{{#unless @first}}{{#unless @last}}
   <div class="show-through">
      <span>{{this}}</span>
      <input type="hidden" name="attr_{{downcase this}}_check" class="check" value="0">
      <span name="attr_{{downcase this}}_owned" class="red">0</span>
      <span name="attr_{{downcase this}}_needed" class="red">0</span>
   </div>                    
   {{/unless}}{{/unless}}{{/each}}
</div>Code language: Handlebars (handlebars)

The ladder variable is defined in carrington.json, an this loops through them to create each needed div.

For this to work, a very complex sheet worker is needed (which will not be the most complex worker in this section!).

// build a string containing change: for each of the 12 abilities, 
//    without having to type them all out.
const changes_abilities = abilities.reduce((changes, item) => 
    `${changes} change:${item}`, '').toLowerCase();

on(`${changes_abilities} change:repeating_abilities remove:repeating_abilities change:character_rank sheet:opened`, () => {
   getSectionIDs('repeating_abilities', idskills => {
      const extra_ranks = idskills.reduce((a,id) => 
          [...a, `repeating_abilities_${id}_rank`], []);
      getAttrs(['character_rank', ...abilities, ...extra_ranks], v => {
         const output = {};
         const character_rank = v.character_rank;
         const rank_index = ranks.indexOf(character_rank) || 0;
         const ranks_at_each = {
            Terrible: 0,
            Poor: Math.max(0, abilities.length + extra_ranks.length - 
                6 - (rank_index-1)),
            Fair: 3 + (rank_index >= 2 ? 1 : 0),
            Good: 2 + (rank_index >= 3 ? 1 : 0),
            Great: 1 + (rank_index >= 4 ? 1 : 0),
            Superb: 0 + (rank_index >= 5 ? 1 : 0),
            Spectacular: 0 + (rank_index >= 6 ? 1 : 0),
            Legendary: 0 + (rank_index >= 7 ? 1 : 0)
         };
                
         ranks.forEach((rank, index) => {
            const count = Object.values(v).filter(r => r === rank).length;
            output[`${rank}_owned`] = 
                count - ((rank === character_rank) ? 1 : 0);
            output[`${rank}_needed`] = ranks_at_each[rank];
            output[`${rank}_check`] = 
                output[`${rank}_owned`] === output[`${rank}_needed`] ? 0 : 1;
         });
         setAttrs(output);
      });
   });
});Code language: JavaScript (javascript)

Since a repeating section has been added for custom abilities, we need to account for that. We launch this worker when the rank of any Ability changes, or when the repeating section changes. We also need to compare abilities against that allowed by character_rank, so we grab that attribute too.

ranks_at_each is a special object that contains every rank, and the number of abilities a character should have. That is a fixed amount plus 1 if the rank is above a threshold (if your character rank is at least Good, you get one more Good Ability, and so on).

The ranks.forEach routine calculates the hidden unput and sets it to 1 or 0.

Recall that getAttrs creates an object variable named v (usually named values, but I change it to v to save on typing). Object.values(v) extracts all the values from that object, and filter searches through those and counts any matching values. So this line counts how many Abilities of a given rank exist.

The next line saves that count in output. The bit about character_rank is because Object.values(v) includes the character_rank, which is on the same scale as Abilities, so one rank will have a count that is one too high.

The output[`${rank}_needed`] = ranks_at_each[rank] simply extracts the number calculated earlier, and saves it to output.

Then, those two values are compared, and if they are different, a value of 1 is saved; otherwise 0.

Finally, the output object is run through setAttrs, updating the sheet.

There’s a lot of complexity here, and this sheet worker is wotrth studying if you need to imptrove your JavaScript knowledge.

The XP Total and Buttons

There is some very complex code hidden in sheet workers here. In overview, there are three XP buttons to go with an input showing your current XP total.

Confirm XP

At the end of each session you fill in answeres to some questions at the top of the middle column. These are checkboxes with a value of 1. There’s also an input that takes the total of the achievements, a set of 10 checkboxes.

When you click XP, the following things happen:

  • The total of those XP are combined, and added to the ne XP Total box.
  • The XP boxes are zeroed out, so they rae ready for the next session.
  • All achievements currently checked also gain a value in achievementX_stop, witith X being 0-9. This is a hidden bo that has a value of 0, and ensures that you can’t get points for the same achievement twice.

The sheet worker for this is relatively simple:

on('clicked:confirm', () => {
   getAttrs(['XP', ...seq(4).map(i => `question${i}`), 
         ...seq(10).map(i => `achievement${i}`)], v => {
      let total = int(v.XP);
      const output = {};
      seq(4).forEach(i => {
         total += int(v[`question${i}`]);
         output[`question${i}`] = 0;
      });
      seq(10).forEach(i => {
         const lock = int(v[`achievement${i}`]);
         if(lock) {
            output[`achievement${i}_stop`] = 1;
         }
      });
      output.XP = Math.min(10,total);
      setAttrs(output);
   });
});Code language: JavaScript (javascript)

This uses the seq() and int() functions, which must be inserted in the script block before this work. That looks like:

const seq = (length, start = 0) => 
   [...Array(length).keys()].map(i => i += start);
const int = (score, fallback = 0) => parseInt(score) || fallback;Code language: JavaScript (javascript)

The seq() function builds an array of nemes. seq(3) would create [0, 1, 2]. This is handy when you need a list of sequential numbers. By default it starts at 0, but you can change the starting number.

The int() function converts a string into a number. So you can do int(v.something) instead of parseInt(v.something) || 0. It saves on the typing.

The map function takes an array, and transforms each element in the array. Here we have a set of numbers, and use them to create a set of attribute names – instead of typing ['question0', 'question1', 'question2', 'question3'], we can type ...seq(4).map(i => `question${i}`). While this does save on typing, it doesn’t make a lot of difference here, but for the 10 attributes of achievements makes a big difference.

With that context, the rest of the worker should be understandabe.

Buying An Ability Advance

This is porbably the most complex code in the character sheet, and since this is already a gargantuan post, the next post will be dedicated to it.

Buy an Extra Advance

Extras are described in detail under Carrington Experience – for the sheet, you only need to know how and when you can buy them. But for this, we use two sheet workers and some CSS.

The HTML Block

First, a block of checkboxes is created. There are five, but 0 to 5 are shown based on your extra_level attribute. As a HTML block, that looks like this:

<div>
   <input type="hidden" name="attr_extra_level" value="0" class="extra-level">
   <span class="rank">Extras</span>
   <input type="hidden" name="attr_extra_level_0" value="0" class="extra-hide">
   <input type="checkbox" name="attr_extra_0" class="noclick extra" value="1" />
   <input type="hidden" name="attr_extra_level_1" value="0" class="extra-hide">
   <input type="checkbox" name="attr_extra_1" class="noclick extra" value="1" />
   <input type="hidden" name="attr_extra_level_2" value="0" class="extra-hide">
   <input type="checkbox" name="attr_extra_2" class="noclick extra" value="1" />
   <input type="hidden" name="attr_extra_level_3" value="0" class="extra-hide">
   <input type="checkbox" name="attr_extra_3" class="noclick extra" value="1" />
   <input type="hidden" name="attr_extra_level_4" value="0" class="extra-hide">
   <input type="checkbox" name="attr_extra_4" class="noclick extra" value="1" />
</div>Code language: HTML, XML (xml)

For each level from 0 to 4, that creates two inputs: one hidden, extra_level_X, which is where that checkbox is displayed, and one visible, extra_x, which is whether that advance has yet been bought.

We could build that manually, but we actually used this Handlebars block:

<div>
   <input type="hidden" name="attr_extra_level" value="0" class="extra-level">
   <span class="rank">Extras</span>
   {{#repeat count=5 start=0}}
      <input type="hidden" name="attr_extra_level_{{@index}}" 
          value="0" class="extra-hide">
      <input type="checkbox" name="attr_extra_{{@index}}" 
          class="noclick extra" value="1" />
   {{/repeat}}
</div>Code language: Handlebars (handlebars)

The CSS to decide which Checkboxes are Displayed

Once we have that block, we need some CSS – so that when the hidden block as a value of 1, the visible checkbox is hidden. This is very simple:

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

The + operator here indicates this rule only applies to first element which directly follows and matches the rule. So we can use this rule once and have it apply to all of the extra attributes.

The first Sheet Worker, again to decide which Checkboxes are Visible

Now, we need two sheet workers. First, we need to display from 0 to 5 checkboxes, so that the progress of buying advances can be measured.

There is an attribute which contains how many Extras you have already, which ranges form 0 to 5. The sheet worker checks how many you have, and makes 1 more checkbox visible. If you have 2 Extras, 3 boxes are visible. If you have 5 Extras, you can’t buy any more, so the number of visible boxes is zero.

on('change:extra_level', () => {
   getAttrs(['extra_level'], v => {
      const extra_level = int(v.extra_level);
      const visible_advances = extra_level > 4 ? 0 : Math.max(1, extra_level +1);
      const output = {};
      seq(5).forEach(i => {
         output[`extra_level_${i}`] = (i < visible_advances) ? 1 : 0;
         output[`extra_${i}`] = 0;
      });
      setAttrs(output);
   });
});Code language: JavaScript (javascript)

This also sets all of the checkboxes to have a value of 0. The level has just increased, so they should all be zeroed out ready for buying new advances.

Now we can finally handle that Button to Buy Advances in the second Sheet Worker

When you buy an advance, the next visible checkbox has its value set to 0. This can be complicated, because you might have 5 visible boxes, and 2 checked. So the sheet worker counts through how many are checked and marks the next one (see the mark variable).

on('clicked:advance_extra', () => {   
   getAttrs(['XP', 'extra_level', ...seq(5).map(i => `extra_${i}`)], v => {
      const xp = int(v.XP);
      if(xp <5) return;
      const output = {};
      output.xp = xp - 5;

      const level = int(v.extra_level);

      // count how many advances are currenly checked.
      let advances = 0;
      seq(5).forEach(i => {
         advances += int(v[`extra_${i}`]);
      });

      const mark = advances +1;

      // mark the next advance
      if(mark > level) {
         output.extra_level = level +1;
      } else {
         seq(5).forEach(i => {
            output[`extra_${i}`] = (i < mark) ? 1 : 0;
         });
      }
      setAttrs(output);
   });
});Code language: JavaScript (javascript)

If there are less than 5 XP available, you can’t buy an advance (they cost 5 XP), so we check for that.

Buy an Ability Advance, or Manually Change Character Rank

This might contain the most complex code on the entire character sheet.

Code language: JavaScript (javascript)

The Extras Total

This is very simple. The code in the earlier Extra Button sets an extra_level attribute. This tells us how many Extras you should have, and this is displayed here. If you have the wrong number, this text is coloured using CSS. This is a handy guide to help keep you accurate.

In Conclusion

There’s a lot of heavy code for this part iof the sheet, and we’ll look at the heaviest in the next post on the innocuous looking Buy an Abillity Advance button.

Series Navigation<< Managing Experience in the Carrington Sheet

Leave a Reply

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