CRP Rolls with Memory

In the last post, we looked at how a player might roll dice for Pendragon. But that is a game where opposed rolls are absolutely required. How can we do that in Roll20?

Opposed Rolls are laborious to implement in a VTT, but Custom Roll Parsing offers an interesting way to manage these very smoothly. This procedure depends on first being able to save a roll, and we’ll see how to do that in this post.

Once you can save a roll, you have a Roll With Memory.

The Pendragon Roll

In the last post, we described how to build the roll for the Pendragon game system. As we proceed, we’ll need to edit that original code. You may find it useful to refer back to that post.

There was a limitation in the last code – it didn’t show exactly what you rolled (Sword, Valourous, or whatever). When we refer back to an earlier roll, it’s handy to be able to jog the memory, so well tweak the original code to link rolls to attributes, like this:

<button type="action" name="act_sword">
   <span>Sword</span>
</button>
<input type="number" name="attr_sword" value="10">

<button type="action" name="act_haft">
   <span>Hafted Weapon</span>
</button>
<input type="number" name="attr_haft" value="10">Code language: HTML, XML (xml)

Every skill, trait, passion, and stat will have a setup something like this, where the button name is identical to the skill. Then the script block can include an object with the attribute names giving extra details like this:

const ratings = {
   sword: "Sword",
   haft: "Hafted Weapon"
}Code language: JavaScript (javascript)

You can use the action button name to get details of the attribute, like @{sword} gives the value, as goes getAttrs(['sword'], and ${haft} in a template literal gives the pretty name (containing spaces and capital letters) that would cause complications in a variable name.

The previous example used this:

const roll_string = "&{template:pendragon} {{title=Example Roll}} {{die=[[1d20]]}} {{score=[[?{skill|10}]]}} {{modifier=[[?{modifier|0}]]}}"Code language: JavaScript (javascript)

but let’s say we loop through that ratings object like this:

Object.keys(ratings).forEach(button => {Code language: JavaScript (javascript)

We could replace that roll_string variable with

const roll_string = `&{template:pendragon} {{title=${<strong>ratings[button]</strong>} Roll}} {{die=[[1d20]]}} {{score=[[@{${<strong>button</strong>}}]]}} {{modifier=[[?{modifier|0}]]}}`Code language: JavaScript (javascript)

This reduces the queries players have to type in and gives more information on the output (the rating name, so you can see what you were actually rolling). output would now look like

In a real game, the actual scores would likely be higher, but you can see now that the rating we use is displayed.

Creating a Memory

We store details of the roll in attributes, and then choose how (or even whether) to display them in inputs or textareas. We need to pick what details to save. We’ll store the rating (sword, energetic, etc), the score in that rating, the modifier used in the roll, and the die roll. With those values we can recreate the roll.

So, we’ll create some inputs to hold the saved values, like this:

<input type="hidden" name="attr_last_rating" value="">
<input type="hidden" name="attr_last_score" value="">
<input type="hidden" name="attr_last_modifier" value="">
<input type="hidden" name="attr_last_die" value="">Code language: HTML, XML (xml)

Once we have a place to store values, we need a way to store those values. In our sheet worker, we’ll need to add something like this:

const output = {
   last_rating: rating,
   last_score: score,
   last_modifier: modifier,
   last_die: die
};
setAttrs(output);Code language: JavaScript (javascript)

To accommodate these, we’ll need to modify the code in the last post.

Recall Last Roll

To demonstrate the last roll is saved, we need to display it in chat. First, we’ll need to add a button to print the last roll, something like this:

<button type="action" name="act_last">Show Last Roll</button>Code language: HTML, XML (xml)

Then we need to modify the sheet worker. We need one sheet worker to make new rolls for any rating – essentially doing exactly what the last button used to do, but now displaying exactly which rating was used.

We also need a sheet worker to respond to the new button we’ve added. Finally, we take the guts of the roll and port it into a new function, so it can be used by both those sheet workers, to avoid needlessly duplicating a lot of code.

We start by creating a sheet worker to initiate a new roll.

Object.keys(ratings).forEach(rating => {
    on(`clicked:${rating}`, () => {
        const roll_string = `&{template:pendragon} {{title=${ratings[rating]} Roll}} {{die=[[1d20]]}} {{score=[[@{${rating}}]]}} {{modifier=[[?{modifier|0}]]}}{{who=@{character_name} }}`;

        do_roll(roll_string, rating);
    });
});Code language: JavaScript (javascript)

Notice this generates two important pieces of information – the rating, and the roll (roll_string). Then we will send them to the function do_roll, which looks like this:

const do_roll = (roll_string, rating = '') => { 
    startRoll(roll_string, pendragon => {
        const die = pendragon.results.die.result;
        const score_base = pendragon.results.score.result;
        const score = (String(score_base).indexOf("+") !== -1) ? (score_base.split('+')[0] ) : +score_base;
        const modifier = pendragon.results.modifier.result ;

        const temp_total = score + modifier;
        const target = Math.max(1, Math.min(20, temp_total));
        const bonus = Math.min(20, Math.max(0, temp_total -20));
        const roll = Math.max(0, Math.min(20, die + bonus));
        const result = roll === target ? 'Critical' : 
                roll === 20 ? 'Fumble' : 
                roll > target ? 'Failure' : `Success (${roll})`;

        if (rating) {
            const output = {
                last_rating: rating,
                last_score: score,
                last_modifier: modifier,
                last_die: die
            };
            setAttrs(output);
        }
        
        finishRoll(pendragon.rollId, {
            score: (score > 20 ? `20 +${score-20}` : score) + 
              (modifier != 0 ? ` ${modifier >= 0 ? '+' : ''}${modifier}`: '') ,
            die: String.fromCharCode(96 + die),
            modifier: result
        });
    });
};Code language: JavaScript (javascript)

This is the startRoll function from the last code, broken out into its own function. There are two extra steps.

const do_roll = (roll_string, rating = '') => { Code language: JavaScript (javascript)

In the first line of the function, we create the names of two parameters – the roll_string, and the rating name. The way rating is written (rating = '') means it has a default value of ”, and so a rating does not have to be applied. That will soon be important, but isn’t relevant yet.

Then at the end of the function, we include the code to save the roll’s details.

        if (rating) {
            const output = {
                last_rating: rating,
                last_score: score,
                last_modifier: modifier,
                last_die: die
            };
            setAttrs(output);
        }Code language: JavaScript (javascript)

Here we store properties into the output variable, then use setAttrs to save them to the character sheet. But it starts with if(rating) – this means this part only runs if a rating is supplied. If we send the default ” value, this part isn’t saved. That comes handy in the next step.

Now we create a sheet worker to recall the last roll.

on('clicked:last', () => {
    getAttrs(['last_rating', 'last_die', 'last_score', 'last_modifier'], v => {
        const roll_string = `&{template:pendragon} {{title=${ratings[v.last_rating]} Roll}} {{die=[[${v.last_die}]]}} {{score=[[${v.last_score}]]}} {{modifier=[[${v.last_modifier}]]}} {{who=@{character_name} }}`;

        do_roll(roll_string);
    });
});Code language: JavaScript (javascript)

The first step here is a getAttrs function to examine the character and grab saved values, then we slot those values into the roll string. Then we call to do_roll function, triggering a new roll – with our supplied values.

We don’t supply a rating here since that’s already stored – we don’t want to overwrite it. So this creates a new roll exactly as before. Below we can see a new roll (left), and then that same roll recalled later. There is no visible difference between them.

Next Step – Opposed Rolls

Now that we can save rolls, we can move on to the final step – actually making opposed rolls. We’ll look at one way to do that in the next post.

Leave a Reply

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