Combining Repeating Sections and Global Stats

A common desire for a character sheet is the ability to create different kinds of buffs, and switch them off and on as desired. That’s a good way to show how global stats and repeating sections interact.

The Buff Repeating Section

Anything on the character sheet must be created in HTML, and using CSS to style it is way easier than alternatives. Here’s a repeating section that assumes a stat with 4 stats, Might, Speed, Smart, and Will.

<h3>Stat Buffs and Banes</h3>
<div class="buffs">
   <h4>Name</h4>
   <h4>Might</h4>
   <h4>Speed</4>
   <h4>Smart</h4>
   <h4>Will</h4>
   <h4>Active</h4>
</div>
<fieldset class="repeating_buffs">
   <div class="buffs">
      <input type="text" name="attr_name" value="" placeholder="NAME">
      <input type="number" name="attr_might" value="0">
      <input type="number" name="attr_speed" value="0">
      <input type="number" name="attr_smart" value="0">
      <input type="number" name="attr_will" value="0">
      <input type="checkbox" name="attr_active" value="1">
   </div>
</fieldset>Code language: HTML, XML (xml)
div.buffs {
  display:grid;
  grid-template-columns: 100px repeat(4, 50px) 30px;
  column-gap: 5px;
}Code language: CSS (css)

While I’ve shown the repeating section first, it will come after the stiff below.

The Global Stats

Elsewhere on the sheet, the stats must be listed – their name, base scores and a box for modifier. This modifier will be used in various places – like for attacks, saving throws, skills, etc. So the repeating section code will be set up to modify it. Before we see how that’s done, lets build the base HTML and CSS.

<div class="base-stats">
   <h4>Stat</h4>
   <h4>Score</h4>
   <h4>Mod</h4>
   <h4>Total</h4>

   <span>Might</span>
   <input type="number" name="attr_might_score" value="0">
   <input type="number" name="attr_might_mod" value="0">
   <input type="number" name="attr_might_total" value="0">

   <span>Speed</span>
   <input type="number" name="attr_speed_score" value="0">
   <input type="number" name="attr_speed_mod" value="0">
   <input type="number" name="attr_speed_total" value="0">

   <span>Smart</span>
   <input type="number" name="attr_smart_score" value="0">
   <input type="number" name="attr_smart_mod" value="0">
   <input type="number" name="attr_smart_total" value="0">

   <span>Wukk</span>
   <input type="number" name="attr_will_score" value="0">
   <input type="number" name="attr_will_mod" value="0">
   <input type="number" name="attr_will_total" value="0">
</div>Code language: HTML, XML (xml)

Notice there are 4 columns. One each for the Stat Name and the Score. Then there isthe Stat Mod, which is calculated from the stat score. Finally there is a Total column – this is for the total of all modifiers the apply – the Mod and any buffs from later.

/* here again grid is used, with only 4 columns, the stats will be split into nice columns */
.base-stats {
   display: grid;
   grid-template-columns: 100px repeat(3, 50px);
   column-gap: 5px;
}Code language: CSS (css)

This CSS makes sure the proper number of columns are used. As usual, if there is a CSS Reset it should be placed right at the top of the CSS file.

On To The Sheet Worker

With the basic stats set up, we can start to build the sheet worker.

The Event Line

Here’s one way to build the event line. Notice there’s a lot of repeated text here.

on('change:might_mod change:speed_mod change:smart_mod change:will_mod change:repeating_buffs:might change:repeating_buffs:speed change:repeating_buffs:smart change:repeating_buffs:will change:repeating_buffs:active, () => {Code language: JavaScript (javascript)

We need to include the base stats, because they affect the total modifier.

Since there is a lot of repeated text, we can build this to reduce our typing:

const stats = ['might', 'speed', 'smart', 'will'];
on(`${stats.map(stat => `change:${stat}_mod change:repeating_buffs:${stat}`).join(' ')} change:repeating_buffs:active`, () => {Code language: JavaScript (javascript)

This looks complicated, so lets descripe what is happening here.

map is a function that works on every element in an arry. We start with a stats array that has the four stats. This function will do something on each element. Here’s that function:

stats.map(stat => `change:${stat}_mod change:repeating_buffs:${stat}`)Code language: JavaScript (javascript)

So it takes a stat like might and creates change:might_mod change:repeating_buffs:might_buff

But we have a problem. map works on an array, and produces an array with the same number of elements. We need the output to be a string. That’s where join comes in. It turns an array into a string – whether is in brackets is used to separate the elements, so join(‘ ‘) creates a space between each element.

By putting all this inside a template literal (`some text ${javascript } some more text`) we get to embed that function in a string.

Getting The Fields

When one of the global stats change, that modifier miht affect every row of the repeating section. So we need to use getSectionIDs to be able to work on every row of the section.

getSectionIDs('repeating_buffs', ids => {
    const fields = [];
    ids.forEach(id => {
       stats.forEach(stat => {
           fields.push(section_name('buffs', id, stat)); 
       });
       fields.push(section_name('buffs', id, 'active'));
    });Code language: JavaScript (javascript)

Here you can see two forEach loops. One to loop through every row (the ids), and another to loop through all the stats. We also see the beauty of using a function like section_name which we copy from last post. We don’t need to type the full name manually (repeating_buffs_${id}_might) – the function works that out for us.

Now we need to supply thos attributes and the gloabal attributes to getAttrs. We could type them manually like this:

getAttrs(['might_mod', 'speed_mod', 'smart_mod', 'will_mod', ...fields], values => {Code language: JavaScript (javascript)

fields is the full set of rrepeating section attributes, and … is the spread operator which takes an array, and spreads it out into its elements. Very handy.

But we already have the stats array so we can do this:

getAttrs([...stats.map(stat => `${stat}_mod`), ...fields], values => {Code language: JavaScript (javascript)

Once again, map transforms every element in an array, so with the spread operator we can get every element of the array transformed as we need it.

The Worker Body

getAttrs takes stat names, looks at the character sheet, and grabs their values. Now we’ve done that, we can build the actual work of our buffs.

We start with this framework. First we create an obect variable to hold the final attribute names and values (output). Then we are going to have to loop through every row of the section – thats what ids.forEach does. finally we update the sheet with the new values (setAttrs).

const output = {}
ids.forEach(id => {

});
setAttrs(output);Code language: JavaScript (javascript)

With the framework, we can work out the code for the loop. Remember we want to calculate the total bonus from buffs. For each stat.

const output = {}
ids.forEach(id => {
   stats.forEach(stat => {
      // first start with the global stat modifiers.
      output[`${stat}_total`] = +values[`${stat}_mod`] || 0;
      // check if this buff is active. The active stat has a value of 1 or 0.
      const active = section_name('buffs', id, 'active');
      if(active) {
      // if the buff is active, add it to the total
         const stat_mod = +values[section_name('buffs', id, stat)]||0;
         output[`${stat}_total`] += stat_mod;
      }
   });
});
setAttrs(output);Code language: JavaScript (javascript)

Note we can use += to add values together.

The Complete Worker

Putting it all together, we end up with a worker like this.

const stats = ['might', 'speed', 'smart', 'will'];
on(`${stats.map(stat => `change:${stat}_mod change:repeating_buffs:${stat}`).join(' ')} change:repeating_buffs:active`, () => {
   getSectionIDs('repeating_buffs', ids => {
      const fields = [];
      ids.forEach(id => {
         stats.forEach(stat => {
            fields.push(section_name('buffs', id, stat)); 
         });
         fields.push(section_name('buffs', id, 'active'));
      });
      getAttrs([...stats.map(stat => `${stat}_mod`), ...fields], values => {
         const output = {};
         ids.forEach(id => {
           stats.forEach(stat => {
              output[`${stat}_total`] = +values[`${stat}_mod`] || 0;
              const active = section_name('buffs', id, 'active');
              if(active) {
                 const stat_mod = +values[section_name('buffs', id, stat)]||0;
                 output[`${stat}_total`] += stat_mod;
              }
            });
         });
         setAttrs(output);
      });
   });
});Code language: JavaScript (javascript)

This section can be used for bonuses and penalties, so it’s versatile. You could also could add a cost and other things like a humanity cost as appropriate to your system.

If we wanted to show off, we could use some more advanced functions and make this shorter, but that’s not the point of this post. There’s plenty of advanced stuff going on here. But the techniques here are easily copied from one repeating section worker to the next. You don’t really need to know how to build the function, as long as you can copy it.

In the next post we’ll look at a thorny problem – what happens when you want to use two or more repeating sections at the same time?

Leave a Reply

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