CRP Opposed Rolls With Memory In Sorcerer

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

We have already seen how we might do this using Custom Roll Parsing in the game pendragon. But sorcerer has another issue: in combat (or when timing matters), you need to roll all the dice at the start of a turn, no matter how many characters are involved, and compare them. Then people act in order of best roll to worst.

Here’s how it works in the game:

  1. At the start of the turn, everyone says roughly what they will do, and who they will oppose (if anyone). These statements can be freely revised until everyone is happy. (“A: I try to pick the vault door.” B: “I shoot A” A: “Um, okay, I’ll jump away from the lock and try to punch B”.
  2. Once all actions are settled, the GM assigns which skills people use and any bonus dice, and rolls are made.
  3. Actions are now resolved in order of best roll to worst. Compare dice rolls if in direct opposition.
  4. When a character is attacked and has already acted (higher initiative), roll full Stamina in defence.
  5. When a character is attacked who has not yet acted, he has two options: suck it up and keep doing their previous action and roll 1 die in defence, or completely cancel their declared action and roll Stamina in defence.

The last two points show the importance of initiative and rolling well on that first roll. Stamina is used as a defensive stat not because its describing toughness, but because Stamina is the skill used for all physical actions – so you might be dodging or counter-attacking or however you describe your action. You have flexibility.

Ordering The Actions

All of this is pretty straightforward, except for step 3. No matter how many people are in the scene, you have to order their rolls from high to low. The sorcerer die mechanic is great, but this part is a major pain. If we were using a simple die + bonus (like, say, d20+skill) and comparing totals, it would be quick and easy. (Well, quick-ish – that step one slows things down a lot.) But the sorcerer dice mechanic is a lot trickier.

One way we can approach this (and the method used in this post), is to use the Turn Tracker. You can send rolls to the tracker by adding &{tracker} to any roll string. Here’s how we need to change the roll worker.

skills_sorcerer.forEach(skill => {
   on(`clicked:${skill.toLowerCase()}`, () => {
        const die_calc = die => String.fromCharCode(96 + die); 
        getAttrs([skill], values => {
            const skill_score = +values[skill] || 0;

            const roll_string = `&{template:sorcerer} {{title=@{character_name} rolls ${skill} }} {{skill= ${skill} }} {{score=[[${skill_score}}]] }} {{modifier=[[?{Modifier|0}]]}} {{dice_roll=[[ [[{(${skill_score}+(?{Modifier})),1}kh1]]d10]]}}`;
         
            startRoll(roll_string, sorcerer => {
                const dice = sorcerer.results.dice_roll.dice;
                const modifier = sorcerer.results.modifier.result;
                const sorted = Array.from(dice).sort((a,b) => b-a);

                const print_out = sorted.map(a => die_calc(a)).join('');
            
                finishRoll(sorcerer.rollId, {
                    skill: skill_score,
                    dice_roll: print_out
                }); 
                // this is the bit we need to add
                startRoll(`![[${sorted.join('')}&{tracker}]]`, tracker_roll => {
                    finishRoll(tracker_roll.rollId);
                });
            });
        });
    });
});Code language: JavaScript (javascript)

Notice we have another startRoll function inside the first one. We need to send the calculated product of a roll to the turn tracker, but we can’t easy access that. One we we can is by using a second startRoll function.

That will give us a turn tracker that looks something like this:

This is a turn with just two characters, and you can see their rolls but there are two complications.

First, the numbers shown are not actually numbers, but will be treated as numbers. They are strings of dice. If you try to use the automated search, that number starting with 6’s will be deemed higher, because the ‘number’ represented is bigger. So, if you want to sort characters, you need to do it by manually dragging a person up or down. Luckily this is easy.

Secondly, dice results of 10 are two-digit numbers. But whenever you see a 1 followed by 0 you know its a ten. Also, the dive are always sorted, so those 10s always appear at the left side.

Now you have a tool where you can roll every character involved in the scene, and display all their rolls at once. The GM can manually sort those characters from high to low, and you are ready to go.

Steps 4 and 5 in the turn sequence suggest new dice rolls are needed. You can easily (and optionally) have extra rolls that don’t get added to the turn tracker, like this:

skills_sorcerer.forEach(skill => {
    on(`clicked:${skill.toLowerCase()}`, () => {
        sorcerer_roll(skill, true);    
    });
    on(`clicked:${skill.toLowerCase()}_later`, () => {
        sorcerer_roll(skill, false);    
    });
});

const sorcerer_roll = (skill, init) => {
    const die_calc = die => String.fromCharCode(96 + die); 
    getAttrs([skill], values => {
        const skill_score = +values[skill] || 0;

        const roll_string = `&{template:sorcerer} {{title=@{character_name} rolls ${skill} }} {{skill= ${skill} }} {{score=[[${skill_score}}]] }} {{modifier=[[?{Modifier|0}]]}} {{dice_roll=[[ [[{(${skill_score}+(?{Modifier})),1}kh1]]d10]]}}`;
         
        startRoll(roll_string, sorcerer => {
            const dice = sorcerer.results.dice_roll.dice;
            const modifier = sorcerer.results.modifier.result;
            const sorted = Array.from(dice).sort((a,b) => b-a);

            const print_out = sorted.map(a => die_calc(a)).join('');
            
            finishRoll(sorcerer.rollId, {
                skill: skill_score,
                dice_roll: print_out
            }); 
            if(later) {
                startRoll(`![[${sorted.join('')}&{tracker}]]`, tracker_roll => {
                    finishRoll(tracker_roll.rollId);
                });
            }
        });
    });
};Code language: JavaScript (javascript)

Here we assume you have a series of buttons named [skill]_later, like this:

<button type="action" name="act_lore_later">
   <span>Lore (Later)</span>
</button>Code language: HTML, XML (xml)

And we have moved the majority of the code off into a separate function so that we don’t have to duplicate code. Now we have two buttons each calling the same function, but the last startRoll function (the one which adds to the turn tracker) is only called when needed.

There are other ways you can represent this kind of thing on the character sheet, but the basic concept is: if you want a roll that doesn’t add to the turn tracker, you need a second worker or a modified version of the main worker, along with a second button to trigger it.

So, there we have a way to make it easy for the GM to handle initiative, a very thorny problem in Sorcerer. But we can go further, by creating rolls with memory.

Sorcerer Roll with Memory

As demonstrated earlier, a roll with memory is a roll which stores details of a roll that you can do things with later. In this code, we will store the used skill, the roll, and any modifier. For this, we just need to add a setAttrs function to the above function, and some inputs to store those values.

<input type="hidden" name="attr_sorcerer_roll" value="">
<input type="hidden" name="attr_sorcerer_skill" value="">
<input type="hidden" name="attr_sorcerer_modifier" value="0">Code language: HTML, XML (xml)

The inputs are easy – we just need to make sure they are hidden. There’s no need for players to interact with these.

For the setAttrs part, we can modify the earlier function. Here we place it between the finishRoll function and if statements.

            finishRoll(sorcerer.rollId, {
                skill: skill_score,
                dice_roll: print_out
            });
            setAttrs({
                sorcerer_roll: dice,
                sorcerer_skill: skill,
                sorcerer_modifier: modifier
            }); 
            if(init) {
                startRoll(`![[${sorted.join('')}&{tracker}]]`, tracker_roll => {
                    finishRoll(tracker_roll.rollId);
                });
            }Code language: JavaScript (javascript)

Now, whenever a roll is made, key pieces of information are stored. Now, we just need to figure out how to use them.

Note that sorcerer_roll contains the dice values in this format: 7, 4, 6, 2: a comma-separated list of numbers, that can be easily treated as an array. That is handy. It is also stored in the order of dice rolled, which is useful if we need to reduce the dice rolled – we can just drop the last few items (or maybe first few – that is probably easier – the side doesn’t matter as long as we are consistent).

Defensive Rolls

After making the rolls at the start of a turn, you can easily be made to roll again. You’ll always be rolling either stamina, or exactly one die. Here’s how it works, again:

  1. If someone attacks you and your initiative is higher, roll full Stamina in defence.
  2. If someone attacks you and your initiative is lower, choose either: to suck it up and keep doing their previous action and roll 1 die in defence, or completely cancel your declared action and roll Stamina in defence.

In all cases, the attacker has already rolled and their dice are used. So we go down the acting characters in order of initiative. They have already said who they are attacking (if, indeed, it is an attack), so they just need to be matched up with their target. That target chooses how they respond – whether they roll one die or their Stamina, and compare that new roll to the attacker.

So we just need two buttons on the character sheet, which each trigger an appropriate roll (one die or stamina), and then compares it vs the attacker and displays the result.

<button type="action" name="act_defend_1d">
   <span>Suck It Up (1d)</span>
</button>
<button type="action" name="act_defend_stamina">
   <span>Defend with Stamina</span>
</button>Code language: HTML, XML (xml)

The attacker has already rolled, so we don’t need to display that again – if they need a refresher, they can look at the turn tracker to see how they did. But we do need to calculate who won, and by how much. Here’s the complete updated sheet worker, and an example of how it is used follows.

skills_sorcerer.forEach(skill => {
    on(`clicked:${skill.toLowerCase()}`, () => {
        getAttrs([skill], values => {
            const skill_score = +values[skill] || 0;
        
            const roll_string = `&{template:sorcerer} {{title=[[0@{selected|token_name}]] }} {{skill= ${skill} }} {{score=[[${skill_score}}]] }} {{modifier=[[?{Modifier|0}]]}} {{dice_roll=[[ [[{(${skill_score}+(?{Modifier})),1}kh1]]d10]]}}`;
            sorcerer_roll(skill, true, roll_string);    
        });
    });
    on(`clicked:${skill.toLowerCase()}_later`, () => {
        getAttrs([skill], values => {
            const skill_score = +values[skill] || 0;
            const roll_string = `&{template:sorcerer} {{title=[[0@{selected|token_name}]] }} {{skill= ${skill} }} {{score=[[${skill_score}}]] }} {{modifier=[[?{Modifier|0}]]}} {{dice_roll=[[ [[{(${skill_score}+(?{Modifier})),1}kh1]]d10]]}} {{target_dice=[[@{target|sorcerer_roll} ]] }} {{target_name=[[0@{target|sorcerer_name} ]] }} {{outcome=[[0]]}}`;
            {{{{/noop}}}}
            sorcerer_roll(skill, false, roll_string);    
        });
    });
});
on(`clicked:one_die`, () => {
    {{{{noop}}}}
    const roll_string = `&{template:sorcerer} {{title=[[0@{selected|token_name}]] }} {{skill= Suck It Up }} {{score=[[1]] }} {{modifier=[[?{Modifier|0}]]}} {{dice_roll=[[ [[{(1+(?{Modifier})),1}kh1]]d10]]}} {{target_dice=[[@{target|sorcerer_roll} ]] }} {{target_name=[[0@{target|sorcerer_name} ]] }} {{outcome=[[0]]}}`;
    sorcerer_roll('to Suck It Up', false, roll_string);    
    
});

const sorcerer_roll = (skill, init, roll_string) => {
    const die_calc = die => String.fromCharCode(96 + die);
    const name = (expression) => expression.substring(1); 
    startRoll(roll_string, sorcerer => {
        const dice = sorcerer.results.dice_roll.dice;
        const score = sorcerer.results.score.result;
        const modifier = sorcerer.results.modifier.result;
        const sorted = Array.from(dice).sort((a,b) => b-a);

        const who = name(sorcerer.results.title.expression);
        const title = `${who} rolls ${skill}`;

        const print_out = sorted.map(a => die_calc(a)).join('');
        const output = {
            skill: score,
            dice_roll: print_out,
            title: title
        }
        
        if(init) {
            startRoll(`![[${sorted.join('')}&{tracker}]]`, tracker_roll => {
                finishRoll(tracker_roll.rollId);
            });
            setAttrs({
               sorcerer_roll: dice,
               sorcerer_skill: skill, 
               sorcerer_modifier: modifier,
               sorcerer_name: who
            });
        } else {
            if(sorcerer.results.target_dice.expression) {
                const attacker = name(sorcerer.results.target_name.expression);
                const target_dice = sorcerer.results.target_dice.expression
                    .split(',').map(i => +i || 0);
                const target_dice_sorted = 
                    Array.from(target_dice).sort((a,b) => b-a);
                
                let losing_target = 0;
                let winner = 0;
                let degree = 0;
                let discards = 0;
                for(let i = 0; i < 
                    Math.min(target_dice_sorted.length, sorted.length); i++) {
                    let a = target_dice_sorted[i];
                    let b = sorted[i];
                    if(a > b) {
                        losing_target = b;
                        winner = 1;
                        break;
                    } else if(a < b) {
                        losing_target = a;
                        winner = -1;
                        break;
                    }
                    discards ++;
                };
                if(winner > 0) {
                    target_dice_sorted.forEach(i => {
                        degree += (i > losing_target ? 1 : 0);
                    });
                    degree -= discards;
                    if(degree >= sorted.length) {
                        output.outcome = 
                          `${attacker} wins a Total Success (degree: ${degree}).`;
                    } else {
                        output.outcome = `${attacker} wins (degree: ${degree}).`;
                    }
                } else if(winner < 0) {
                    sorted.forEach(i => {
                        degree += (i > losing_target ? 1 : 0);
                    });
                    degree -= discards;
                    if(degree >= target_dice_sorted.length) {
                        output.outcome = 
                           `${who} wins a Total Success (degree: ${degree}).`;
                    } else {
                        output.outcome = `${who} wins (degree: ${degree}).`;
                    }
                } else {
                    output.outcome = 'Draw.';
                    // this will almost never happen
                }
            }
        }
        finishRoll(sorcerer.rollId, output); 
    });
};Code language: JavaScript (javascript)

You might notice some things have been shifted around in this version of the worker, with good reason, butt he major new code is inside the else statement after if(init). This code could be written more elegantly, but it would be harder to understand.

Example of Play

We a playing a game involving aircraft dogfighting. Hero and Ally are faced by three minions each in their own fighter, and to keep things simple, they all declare they are attacking each other. Two minions are going for the Hero, and one for the Ally.

Everyone makes initial rolls using Stamina, and we see this:

The initial rolls are all automatically added to the turn order, in the order they are rolled. We are using a rule that states everyone using the same character sheet rolls once for initiative, so Minion 3 represents all 3 minions. It’s not necessary, but does makes things a bit simpler.

The GM looks at the turn counter, and sees Hero should be higher up, so that character is dragged to the top. This gives the new turn counter:

Based on the original declaration, Hero is attacking Minion 1, who must choose whether to Suck It Up to keep the original action or abandon it and dodge with full Stamina. He looks at Hero’s dice roll, and thinks a dodge is better. Full stamina dice are rolled, with Hero as the target.

The GM marks the damage on Minion 1. Let’s say that’s enough to take him out (the dogfight has been going on a few rounds).

Now Minions 2 and 3 take their turns. Minion 2 is attacking Hero, who has higher initiative and gets full Stamina against this attack. This is easily avoided (with 3 degree) – the GM should give an extra bonus with such an impressive success. Maybe he gets a bonus to dogfighting dice next turn, or fights back and obliterates this minion (probably only if the minion was already badly damaged).

Then Ally is attacked and can either Suck It Up to keep her action or abandon that action to fully dodge with Stamina. She doesn’t want to lose her action, and looking at the attack (641), it’s not very good, so she decides to suck it up and rolls one die.

She rolls really well, and just avoids the attack (it’s only one die after all). Then she attacks the last minion, and realises her attack isn’t that good either. She would probably have been better off going for a full dodge. But she’s committed now. The minion attempts to dodge the attack:

And fails! Ally’s former roll of 5/4/3/1 is good enough to land one degree – she damages that opponent just slightly.

And then we go into the next round, with Hero and Ally now facing one minion each…

In Conclusion

This system makes Sorcerer much more smooth in play. The code is tricky, but once added to sheet, things move way more smoothly. Just remember to select your token!

Leave a Reply

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