CRP: Spell Slots

In this post, we’ll create an extremely complex project. While it isn’t emulating a specific game, there are lots of techniques here you might copy for a lot of games of a certain style.

First, imagine you have characters with an Arcane Level and a Divine Level (each ranging from 1-10), which is also your stat bonus. It is likely calculated from other stats not shown here. This is your power level with that type of spell.

Spell levels can be from 1-5, and at power level 1, you start with one spell at level 1. Add half your level (rounded down), and for each greater level reduce by 1. So at level 6, you have four level 1 spell slots, three at level 2, two at level 3, and one at level 4. That tells you how many spells you can use from each level. It’s a deliberately easy progression, and a real game would likely not work like this.

Then you have a second set of stat boxes which show how many spells you have available. When you cast a spell of a given slot, mark off one of that box. And periodically you can click a refresh button and recover all used points.

And then, we have a list of spells you can learn, each of which has a type (Arcane or Divine) and Level. Spells can vary dramatically in how they work, and clicking that spell’s button makes any relevant rolls to that, and also expends one spell slot of the appropriate class and level. Players record these in a Repeating Section.

Finally, we’ll also look at how to print to chat a list of chat buttons which show the spell list by class or level, or both.

The Spell Database

You’d have a database of spells (or powers, weapons, whatever), in your sheet worker script block, created as an object variable like this:

const spells = {
        'Magic Missile': {type: 'arcane', level: 1, attack: 'arcane', damage: 'd4xd4', effect: 'fires several bolts of energy', 
            roll: '&{template:default} {{name=Magic Missile}} {{attack=[[1d20+@{arcane}]]}} {{damage=[[1d4 * 1d4]]}}'},
        Sleep: {type: 'arcane', level: 2, damage: 'a/3 HD', effect: 'puts one or more opponents to magical sleep', 
            roll: '&{template:default} {{name=Sleep}} {{effect=Puts [[ [[floor(@{arcane}/3+1)]]d6]] hit dice to sleep}}'},
        Fireball: {type: 'arcane', level: 3, damage: 'd6/arc', effect: 'a burst of flame in a globe', 
            roll: "&{template:default} {{name=Fireball}} {{damage=[[ [[{10,@{arcane}}kl1]]d6]]}} {{effect=Affects a 20' area}}" },
        /* more spell details */
        /* need entries for attr_attack, damage, effect */
    };Code language: JavaScript (javascript)

Notice spells might have properties of type, level, roll, effect, attack, and damage, but some don’t exist for some spells, so we’ll need to be able to deal with that. Note also the keys (the names of spells) follow javascript variable rules – if they contain a space, wrap them in quotes.

Note that attack, damage, and effect are just for their display on the character sheet.

The spells don’t match existing game systems – they are just used for examples. There would be many more spells in the actual game’s list. We’ll cover how to handle a player-created list at the end.

For reasons that will become apparent later, you need to create a button for each spell in the HTML, like so:

<button type="roll" name="attr_Magic Missile" value="&{template:default} {{name=Magic Missile}} {{attack=[[1d20+@{arcane}]]}} {{damage=[[1d4 * 1d4]]}}" class="hidden"></button>
<button type="roll" name="attr_Sleep" value="&{template:default} {{name=Sleep}} {{effect=Puts [[ [[floor(@{arcane}/3+1)]]d6]] hit dice to sleep}}"></button> 
<button type="roll" name="attr_Fireball" value="&{template:default} {{name=Fireball}} {{damage=[[ [[{10,@{arcane}}kl1]]d6]]}} {{effect=Affects a 20' area}}" ></button>Code language: HTML, XML (xml)

Notice their value is the same as the roll in the spell list, the rest is indetincal to each other 9apart from name). Note that spaces are fine here.

The Basic HTML and CSS

We need a place to store the chosen spells. A repeating section is great for a dynamic list. One drawback for this example is we use a select, so every spell needs to be defined in the character sheet.

<div class="spells-list">
   <h4>Name</h4>
   <h4>Type</h4>
   <h4>Level</h4>
   <h4>Attack</h4>
   <h4>Damage</h4>   
</div>
<fieldset class="repeating_spells">
  <details class="spells">
    <summary>
      <div class="spells-list">
        <select name="attr_name">
          <option value="" selected>Pick One!</option>
          <option>Magic Missile</option>
          <option>Sleep</option>
          <option>Fireball</option>
        </select>
        <input type="text" name="attr_type" value="" readonly>
        <input type="number" name="attr_level" value="" readonly>
        <input type="text" name="attr_attack" value="" readonly>
        <input type="text" name="attr_damage" value="" readonly>
        <button type="action" name="act_roll">Roll</button>
      </div>
    </summary>
    <textarea name="attr_effect" readonly></textarea>
  </details>
</fieldset>Code language: HTML, XML (xml)

We’ll create a sheet worker where the player picks a spell name in the select, and the details of all spells are filled in.

Somewhere else on the sheet there’ll be a list of spell levels, showing how many spell slots a character has available. This is a calculated value, from their class or arcane attribute, and probably their spell casting attribute.

We need to record this twice. First, for the normal, fixed maximum (this might or might not be visible), and then the available slots – which are checked off as spells are used. When a character rests (and hits the Spll Reset button), they refresh to their normal totals.

For the two locations, the first set of attributes use the attribute name with a _max suffix. Roll20 supports two attributes with the same name, as long as one is called _max. All attributes are readonly, because they are set by sheet worker.

<div class="base-stats">
   <span>Arcane Level</span>
   <input type="number" name="attr_arcane" value="0" readonly>
   <span>Divine Level</span>
   <input type="number" name="attr_divine" value="0" readonly>
   <button type="action" name="act_reset_spells">Reset Spells</button>
   <button type="action" name="act_menu">Chat Menus</button>
</div>
<h3>Spells Available</h3>
<div class="spell-slots">
<h4>Type and Level</h4>
   <h4>1</h4>
   <h4>2</h4>
   <h4>3</h4>
   <h4>4</h4>
   <h4>5</h4>
   <span>Arcane</span>
   <input type="number" name="attr_arcane1_max" value="0" readonly>
   <input type="number" name="attr_arcane2_max" value="0" readonly>
   <input type="number" name="attr_arcane3_max" value="0" readonly>
   <input type="number" name="attr_arcane4_max" value="0" readonly>
   <input type="number" name="attr_arcane5_max" value="0" readonly>
   <span>Divine</span>
   <input type="number" name="attr_divine1_max" value="0" readonly>
   <input type="number" name="attr_divine2_max" value="0" readonly>
   <input type="number" name="attr_divine3_max" value="0" readonly>
   <input type="number" name="attr_divine4_max" value="0" readonly>
   <input type="number" name="attr_divine5_max" value="0" readonly>
</div>
<h3>Spell Use</h3>
<div class="spell-slots">
   <h4>1</h4>
   <h4>2</h4>
   <h4>3</h4>
   <h4>4</h4>
   <h4>5</h4>
   <span>Arcane</span>
   <input type="number" name="attr_arcane1" value="0" readonly>
   <input type="number" name="attr_arcane2" value="0" readonly>
   <input type="number" name="attr_arcane3" value="0" readonly>
   <input type="number" name="attr_arcane4" value="0" readonly>
   <input type="number" name="attr_arcane5" value="0" readonly>
   <span>Divine</span>
   <input type="number" name="attr_divine1" value="0" readonly>
   <input type="number" name="attr_divine2" value="0" readonly>
   <input type="number" name="attr_divine3" value="0" readonly>
   <input type="number" name="attr_divine4" value="0" readonly>
   <input type="number" name="attr_divine5" value="0" readonly>
</div>Code language: HTML, XML (xml)

For the CSS, we use only the most basic code just to make sure we can see the code is working. Later, we might make it more stylish.

For the spell list, we can add CSS Grid to arrange the columns suitably. Number inputs in roll20 are of a fixed width, and it’s harder to force them to a specific width. Finally, using details let us handle a wider textarea with hardly any code. We can put everything except the textarea in the summary section, so it is shown normally and the player has to click a free space to see the text area.

.spells-list {
  display: grid;
  grid-template-columns: 80px repeat(4, 50px) 30px;
  column-gap: 5px;
}
.spells-list button[type="action"] {
  width: 30px;
}
.ui-dialog .charsheet .spells-list input {
    width: 50px;
    height: 24px;
}
.ui-dialog .charsheet .spells-list select {
    width: 100px;
    height: 24px;
    margin-bottom: 2px;
}
details.spells textarea {
  width: 360px;
  height: 24px;
}Code language: CSS (css)

Inputs and selects are inside the grid area, but Roll20 assigns them dimensions, so we need to overwrite them.

The textarea is made the width of the spell description and given a height to show exactly the number of rows you want to show. Note that textareas can be made larger or smaller, so the player will be able to see whatever text you put here.

For the spell slots, another CSS Grid arrangement will sort out the columns. The width of number inputs is already set above so doesnt have to be added here again.

.spell-slots {
  display: grid;
  grid-template-columns: 100px repeat(5, 3.5em);
  column-gap: 5px;
}Code language: CSS (css)

Here’s what the sheet looks like, with this minimal styling.

The Sheet Workers

Now we move on to the necessary sheet workers.

Calculate Spell Slots

We also need to set the normal spell slots to the same values, and support the Spell Reset button (which refreshes the normal boxes – copies the _max to normal values.

    const int = (score, fallback) => parseInt(score) || fallback;
    on('change:arcane change:divine', () => {
        getAttrs(['arcane', 'divine'], values => {
            const arcane = int(values.arcane);
            const divine = int(values.divine);
            const output = {};

            //calculate how many extra spell slots are gained for arcane or divine
            const arcane_bonus = Math.floor(arcane/2);
            const divine_bonus = Math.floor(divine/2);
            [0, 1, 2, 3, 4].forEach(i => {
                output[`arcane${i +1}_max`] = Math.max(0, arcane_bonus + 1 - i);
                output[`arcane${i +1}`] = Math.max(0, arcane_bonus  + 1 - i);
                output[`divine${i +1}_max`] = Math.max(0, divine_bonus + 1 - i);
                output[`divine${i +1}`] = Math.max(0, divine_bonus + 1 - i);
            });
            setAttrs(output, {silent:true});
        });
    });Code language: JavaScript (javascript)

Fill in Spell Details

Notice how this uses the int function to make those parseInt lines easier to write – it can be reused in later workers.

We also need a worker to fill in the spell stats for display purposes, when the player changes a spell. One good way to do this is to fill in the entire repeating section at once.

    on('change:repeating_spells:name', () => {
        getAttrs(['repeating_spell_name'], values => {
            const name = values['repeating_spell_name'];
            const output = {};
            output['repeating_spell_type'] = spells[name].type;
            output['repeating_spell_level'] = spells[name].level;
            if(spells[name].hasOwnProperty('attack')) {
                output['repeating_spell_attack'] = spells[name].attack;
            } else {
                output['repeating_spell_attack'] = 'N/A';
            }
            if(spells[name].hasOwnProperty('damage')) {
                output['repeating_spell_damage'] = spells[name].damage;
            } else {
                output['repeating_spell_damage'] = 'N/A';
            }
            if(spells[name].hasOwnProperty('effect')) {
                output['repeating_spell_effect'] = spells[name].attack;
            } else {
                output['repeating_spell_effect'] = 'N/A';
            }           
            setAttrs(output);
        });
    });Code language: JavaScript (javascript)

This method works, and is fine – but notice there are several sections of code repeated at the end there. They could be made into a function. You only need to do this if you want your code to be more elegant and have the time.

    on('change:repeating_spells:name', () => {
        getAttrs(['repeating_spell_name'], values => {
            const name = values['repeating_spell_name'];
            const output = {};
            output['repeating_spell_type'] = spells[name].type;
            output['repeating_spell_level'] = spells[name].level;
            // function to get attack , damage, and effect
            // spell is a Javascript Object of that spells data.
            const spell_output = (spell, value, not_found) => {
                let found = '';
                if(spells[name].hasOwnProperty(value)) {
                    found = spells[name][value];
                } else {
                    found = not_found;
                }
                return found;
            }
            output['repeating_spell_attack'] = 
              spell_output(spells[name], 'attack', 'N/A');
            output['repeating_spell_damage'] = 
              spell_output(spells[name], 'damage', 'N/A');
            output['repeating_spell_effect'] = 
              spell_output(spells[name], 'effect', 'N/A');
                     
            setAttrs(output);
        });
    });Code language: JavaScript (javascript)

An approach like this reduces the opportunities for typos and similar errors – since you only reduce the times you type attack, damage, or effect.

That if statement could easily be streamlined to make a ternaty operator:

    on('change:repeating_spells:name', () => {
        getAttrs(['repeating_spell_name'], values => {
            const name = values['repeating_spell_name'];
            const output = {};
            output['repeating_spell_type'] = spells[name].type;
            output['repeating_spell_level'] = spells[name].level;
            // function to get attack , damage, and effect
            // spell is a Javascript Object of that spells data.
            const spell_output = (spell, value, not_found) => {
                let found = spells[name].hasOwnProperty(value) ? 
                  spells[name][value] : not_found;
                return found;
            }
            output['repeating_spell_attack'] = 
              spell_output(spells[name], 'attack', 'N/A');
            output['repeating_spell_damage'] = 
              spell_output(spells[name], 'damage', 'N/A');
            output['repeating_spell_effect'] = 
              spell_output(spells[name], 'effect', 'N/A');
                     
            setAttrs(output);
        });
    });Code language: JavaScript (javascript)

There are other optimisations that could be made. I’;ll leave you to look for those. But bear in mind, this is the kind of thing you do when you are sharing your code for others. If you are working on something purely for your own use, you aremore likely to choose the quick and dirty apprach – and that’s perfectly fine (as long as it works).

Roll For the Spell

Then we need to a Custom Roll Parsing worker that picks which spell is cast, makes the appropriate roll, and marks the appropriate spell slot used.

on('clicked:repeating_spells:roll', event_info => {
        const row_id = event_info.triggerName.split('_')[2];
        getAttrs([`repeating_spells_${row_id}_name`, 'character_name', 
                'arcane1', 'arcane2', 'arcane3', 'arcane4', 'arcane5',
                'divine1', 'divine2', 'divine3', 'divine4', 'divine5'], values => {
            const spell_name = values[`repeating_spells_${row_id}_name`];
            if(!spell_name) return;
            const roll_string = spells[spell_name].roll;
            const spell_type = spells[spell_name].type;
            const spell_level = spells[spell_name].level;
            const spell_slot = `${spell_type}${spell_level}`;
            const spell_number = int(values[spell_slot]);
            console.log({values, spell_type, spell_level, spell_slot, spell_number})
            if(spell_number) {
                startRoll(roll_string, roll => {
                    finishRoll(roll.rollId);
                })
                setAttrs({
                    [spell_slot]: spell_number -1
                });
            } else {
                startRoll(`&{template:default} {{${values.character_name} cannot cast ${spell_name} right now}}`, roll => {
                    finishRoll(roll.rollId);
                })
            }
        });
    });Code language: JavaScript (javascript)

This worker grabs all the arcane and divine slots, to make it easier to select which one is used on this spell roll. It’s easier to collect multiple attributes in a single getAttrs than it is to run the function twice, so it’s easier to get all the attributes you might want in one fell swoop.

This also demonstrates how you don’t have to make a roll when using CRP – you can just send text to chat. You still need the finishRoll function, because without it there’s a big delay on startRoll.

Reset Spells

The button does one thing: it restores all used spell slots back to their maximum. This is why we have two attributes for each slot – to make it easy to restore to the normal total.

const seq = (how_many, start = 1) => [ ...Array(how_many).keys() ].map( increment => increment + start);
on('clicked:menu', () => {
   getAttrs([...seq(5).map(i => `arcane${i}_max`),
             ...seq(5).map(i => `divine${i}_max`)], values => {
      const output = {};
      Object.keys(values).forEach(key => {
         output[key.replace('_max', '')] = values[key];
      });
      setAttrs(output,{silent:true});
   });
});Code language: JavaScript (javascript)

Generate Chat Menus

Finally, we need a worker to generate a list of chat buttons which are sent only the player, so they can look at their spells in chat and cast them from there. This needs to loop through the entire repeating section.

This is radically different from the usual roll buttons, but in principle is very simple.

on('clicked:menu', () => {
  getSectionIDs('repeating_spells', id_array => {
    const names = id_array.reduce((all, id) => 
      [...all, section_name('spells', id, 'name')]
      , []);
    getAttrs([...names, 'character_name'], values => {
      const character = values.character_name;
      // build array of spell names
      const buttons = [];
      id_array.forEach(id => {
        buttons.push(values[section_name('spells', id, 'name')]);
      });
// now create a string from that array of ability calls in the format [name](~character_name|roll) so they work for roll20 buttons.
      const button_string = (buttons) => {
        let output = '{{';
        buttons.forEach(button => {
          output += `[${button}](~${character}|${button}) `;
        });
        return output + '}}';
      };
      const roll_string = 
        `/w ${character} &{template:default} {{name=Spell Buttons}} ${button_string(buttons)}`;
      startRoll(roll_string, roll => {
        finishRoll(roll.rollId);
      });
    });
  });
});Code language: JavaScript (javascript)

There are ways to make this shorter and more elegant, but that would make the process less readable. To understand this, you need to be familiar with repeating sections and using arrays in sheet workers.

When the chat menu button is clicked, a series of buttons are printed to chat, one for each spell.

It;s this routine for which you need one ability button per spell in the HTML. Calling an ability from chat creates extra complications. If you aren’t include a mechanic like this, you don’t need them.

Sorting The Spell Buttons

One drawback is that the spells are listed in the order they are shown in the repeating section, and we probably want them ordered by type (arcane, divine) and spell level. This can be done. It’s the button_string function we need to modify.

But this post is long enogh already, and that’s a topic for another day.

The Roll Template

This post is already huge, but luckily it uses the default rolltemplate so no template has to be built here.

Concluding Words

There are ways to improve on this, like using customisable spell lists. But this post is already long, and that kind of thing can be covered later. For now, feel free to mine it for techniques youuse in your own character sheets.

Series Navigation<< CRP: Essence 20CRP: A Simple Example >>

Leave a Reply

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