Buying an Ability in the Carrington Sheet

At the top of the XP-floater, is a section for tracking advances that looks like this:

You can have a Character rank (Poor, Fair, Good, etc.) and each rank takes 1-6 advances to complete. So we need to track both your current rank, and how many advances you have gained in that rank.

I’ll describe an initial version of the worker, which concentrated on the core function, and then show how it was expanded to cope with other situations.

For the record, the checkboxes have names like ap_fair_1 or ap_superb_3. This worker also uses the seq() custom function, which can be given a number and returns an array of numbers. So, seq(3) gives [0, 1, 2].

Get Rank Indices

To make the sheet worker code simpler, we create a function to create an array of all the attribute names.

const get_rank_indices = () => {
   // function to return the address of every attribute that 
   //   contains an Ability advance
   const fields = [];
   ranks.forEach((rank, index) => {
      if(rank != "Legendary") {
         seq(index).forEach(position => {
            fields.push(`ap_${rank.toLowerCase()}_${position}`);
         });
      }
   });
   return fields;
};Code language: JavaScript (javascript)

Notice how it loops through each rank in an array of ranks, and finds the index of that rank in the array, and creates an array of numbers up to that index.

The array is created earlier in the script block and looks something like:

const ranks = ['Terrible', 'Poor', 'Fair', /* etc */ ];Code language: JavaScript (javascript)

So the ranks already exist so we first loop to find each rank, then loop again through each number from 0 up to the rank, creating an array that creates all the attribute names, like 'ap_poor_0', 'ap_fair_0', 'ap_fair_1', etc.

A Digression

In truth, I originally planned to have two different sheet workers (you’ll see why), and both would need to do this so I created a custom function that would be called in both workers. That way, I only needed to write this code once.

But then, I realised I could combine both sheet workers, so this didn’t need to be a separate function – it could have been made part of the sheet worker. But this is neater – and I’d already written it.

Buying an Advance

This sheet worker starts with the code to be triggered with then button is clicked, then grabs all the attributes. Notice how seq() is used to grab all the attribute names.

on('clicked:advance_ability', (event_info) => {
   getAttrs(['character_rank', 'XP', ...get_rank_indices()], v => {Code language: JavaScript (javascript)

Then we initialise the character_rank, xp, and an output object, and since an advance costs 5 xp, we reduce the xp stat by 5.

      const xp = int(v.XP)
      let rank = v.character_rank;
      let index = ranks.indexOf(rank);
      const output = {};
      output.xp = xp - 5;Code language: JavaScript (javascript)

Now, we know what the current rank is, and need to count how many advances are currently filled in. For reasons we see below, it can never be all of them. But it can be one less, all the way down to 0. So if the character is Good, there are 3 boxes, so 0, 1, or 2 of them may be filled in. We need to count them.

      let advances = 0;
      seq(index).forEach(i => {
         advances += int(v[`ap_${rank.toLowerCase()}_${i}`]);
      });Code language: JavaScript (javascript)

Now we determine the number to be marked, which is one more than the current count.

      const mark = advances +1;Code language: JavaScript (javascript)

Purely for ease, we set all boxes to 0. We loop through all the ranks and set all of its boxes to 0. (There are other approaches, but this seemed like a good idea at the time.)

      seq(7).forEach(rank => {
         const which = ranks.indexOf(ranks[rank]);
         seq(which).forEach(position => {
            output[`ap_${ranks[rank].toLowerCase()}_${position}`] = 0;
         });
      });Code language: JavaScript (javascript)

Since all boxes are now 0, we need to loop through the ranks again and set only those possessed to a value of 1. There are two steps here: all ranks below the current rank are completely filled in, while those at the current rank are filled in only up to the current mark.

      // if buying an advance, add 1
      ranks.forEach(rank => {
         const which = ranks.indexOf(rank);
         if (which < index) {
            seq(which).forEach(position => {
               output[`ap_${rank.toLowerCase()}_${position}`] = 1;
            });
         } else if (which === index) {
            seq(mark).forEach(position => {
               output[`ap_${rank.toLowerCase()}_${position}`] = 1;
            });
         }
      });
      // if have filled all advances of a rank, increase rank.Code language: JavaScript (javascript)

Now, we need to check if all boxes for a rank are filled in. In that case, rank is increased by one step.

      if (mark === index) {
         rank = ranks[index +1];
         output['character_rank'] = rank;
      }
      setAttrs(output);
   });        
});Code language: JavaScript (javascript)

Then we use setAttrs and the sheet worker is finished. Or is it?

Changing the Rank Directly

It turns out that a character can have a character rank set directly. You might decide this character is Fair, Good, Great, or higher. When this is done, all advance boxes below the current level must be filled in. This is the code for that.

         const when_to_stop = ranks.indexOf(rank) -1;
         ranks.forEach(rank => {
            const which = ranks.indexOf(rank);
            if(which <= when_to_stop) {
               seq(which).forEach(position => {
                  output[`ap_${rank.toLowerCase()}_${position}`] = 1;
               });
            }
         });
         output.XP = 0;Code language: JavaScript (javascript)

More Special Conditions

Spectacular Rank

It turns out that when a character reached Spectacular Rank, ecah avdance costs only 1 XP (and they can only get 1 XP per session). Our code needs to account for that.

Combining the Two Sheet Workers

It turns out we can combine the two intended sheet workers (when the button is clicked, and when rank is manually increased), but there’s a problem here.

The obvious approach would be to add change:character_rank, to detect manual rank changes. But rank can increase as a result of clicking the advance ability. This would lead to the worker being triggered twice.

We can stop that, by detecting if the worker is being triggered manually or automatically, using event_info.

on('clicked:advance_ability change:character_rank', (event_info) => {
   if (event_info.sourceType === 'sheetworker') return;
   const trigger = event_info.sourceAttribute; 
   // if source trigger is Character_rank some things work differently
   const use_xp = trigger==='character_rank' ? 0 : 1;
Code language: JavaScript (javascript)

Then we create a variable called use_xp, which is set to 1 if the worker is triggered by clicking the button, and 0 if manually changing rank. We’ll use this variable in the main body of the worker to detect how it was triggered, and run the correct code.

The Complete Sheet Worker

Now we have figured out all the bits of the worker, we can put them together. Here’s the complete sheet worker.

on('clicked:advance_ability change:character_rank', (event_info) => {
   if (event_info.sourceType === 'sheetworker') return;
   const trigger = event_info.sourceAttribute; 
   // if source trigger is Character_rank some things work differently
   const use_xp = trigger==='character_rank' ? 0 : 1;
   getAttrs(['character_rank', 'XP', ...get_rank_indices()], v => {
      const xp = int(v.XP)
      let rank = v.character_rank;
      if(xp < (rank ==="Spectacular" ? 1 : 5) && use_xp) return;
      let index = ranks.indexOf(rank);
      const output = {};
      // reduce XP by the cost of one advance
      if(use_xp) {
         output.xp = xp - (rank ==="Spectacular" ? 1 : 5);
      }

      // count how many advance boxes are marked at this rank
      let advances = 0;
      seq(index).forEach(i => {
         advances += int(v[`ap_${rank.toLowerCase()}_${i}`]);
      });

      // mark this advance (and possibly all lower advances ?)
      const mark = use_xp ? advances +1 : 0;

      // set all advance boxes to zero
      seq(7).forEach(rank => {
         const which = ranks.indexOf(ranks[rank]);
         seq(which).forEach(position => {
            output[`ap_${ranks[rank].toLowerCase()}_${position}`] = 0;
         });
      });
      // if buying an advance, add 1
      if(use_xp) {
         ranks.forEach(rank => {
            const which = ranks.indexOf(rank);
            if (which < index) {
               seq(which).forEach(position => {
                  output[`ap_${rank.toLowerCase()}_${position}`] = 1;
               });
            } else if (which === index) {
               seq(mark).forEach(position => {
                  output[`ap_${rank.toLowerCase()}_${position}`] = 1;
               });
            }
         });
         // if have filled all advances of a rank, increase rank.
         if (mark === index) {
            rank = ranks[index +1];
            output['character_rank'] = rank;
         } 

      } else {
         const when_to_stop = ranks.indexOf(rank) -1;
         ranks.forEach(rank => {
            const which = ranks.indexOf(rank);
            if(which <= when_to_stop) {
               seq(which).forEach(position => {
                  output[`ap_${rank.toLowerCase()}_${position}`] = 1;
               });
            }
         });
         output.XP = 0;
       }
       setAttrs(output);
   });        
});Code language: JavaScript (javascript)

Series Navigation<< How Many Abilities in the Carrington SheetA variety of systems in the Carrington Sheet >>

Leave a Reply

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