CRP: OctaNe and Inspectres

A common request is for a roll that gives its output as text, like a d6 roll with 1 = No, And, 2 = No, 3 = No, But, 4 = yes, But, 5 = Yes, and 6 = Yes, And.

You can create such a table with Rolltables, but if there are modifiers or multiple dice to calculate the total, rollteables quickly prove themselves not to be a good option and there appears to be no way to do this in Roll20. There arer in fact two ways to do this.

The first is to create a custom roll template, using ligic helpers to show the result of the dice (a series of bits like this: {{#rollTotal roll 5}}Yes{{/rollTotal roll 5}}). This approach can be useful, but in these two systemns, you also change an attribute based on the roll result. That can’t be done with a simple roll template. For that you need Custom Roll Parsing.

OctaNe

OctaNe and inspectres both use the same basic concept: you roll one or more d6, take the highest single dice, and look at a table to see what that table generates. In addition, certain results on the roll change an attribute value.

In this post we’ll focus on OctaNe, but it’s the same basic approach for Inspectres. The d6 table looks like this:

  • If the Die Result is 1 or 2, it means the Moderator has total control.
  • If the Die Result is 3, it means the Moderator has partial control.
  • If the Die Result is 4, it means the Player has partial control.
  • If the Die Result is 5 or 6, it means the Player has total control.
  • If the Die Result is 5, the Player gains bonus Plot Points equal to their Style.
  • If the Die Result is 6, the Player gains bonus Plot Points equal to 1 + their Style.

Roll some number of d6s and compare the highest single die on this table.

Exactly what partial and total control mean, and how Styles are used can be left to those who buy the rules. We’ll just show how the mechanic can be represented here.

There is an extra complexity. Sometimes a roll will have Hazards, and the number of dice will be reduced from highest to lowest. There is yet another complexity – if using the Styles of Might or Magic, the Hazardrating can be reduced. We won’t do apply here, but it could be done. It can also be done manually, like the player says, “I’m usign Might, so reduce hazards by 2”. We’ll assume the game is doing that.

And finally a character can spent plot points on a roll, typically up to an amount equal to their style. Each style point grants a bonus d6, but also reduces the points in that style.

The HTML and basic CSS

We need attributes to hold the Styles, say they can be changed. That looks like this:

<div class="styles">
    <button type="action" name="act_daring" class="style">Daring</button>
    <input type="number" name="attr_daring" value="0">
    <button type="action" name="act_ingenuity" class="style">Ingenuity</button>
    <input type="number" name="attr_ingenuity" value="0">
    <button type="action" name="act_craft" class="style">Craft</button>
    <input type="number" name="attr_craft" value="0">
    <button type="action" name="act_charm" class="style">Charm</button>
    <input type="number" name="attr_charm" value="0">
    <button type="action" name="act_might" class="style">Might</button>
    <input type="number" name="attr_might" value="0">
    <button type="action" name="act_magic" class="style">Magic</button>
    <input type="number" name="attr_magic" value="0">
</div>Code language: HTML, XML (xml)

Note that each style name is inside an action button. The CRP worker later will use those.

.styles {
   display: grid;
   grid-template-columns: 100px 30px;
   column-gap: 5px;
}
.charsheet .styles input[type="number"] {
   width: 30px;
}Code language: CSS (css)

As ever, we use CSS Grid to apply the columns – it is so easy (never use tables). Roll20 number inputs need some extra styling to force their width, so we do that here.

The Sheet Worker

As with other complex CRP workers, I’ll break this into sections and erxplain each section, then post the completed worker later.

    const styles = ['daring', 'ingenuity', 'craft', 'cunning', 'might', 'magic'];
    const seq = (how_many, start = 0) => 
       [ ...Array(how_many).keys() ].map( increment => increment + start);
    const capitalize = word => word[0].toUpperCase() + word.slice(1);
    const clicked = buttons => buttons.map(button => `clicked:${button}`).join(' ');
    on(clicked(styles), event_info => {
        const skill = event_info.triggerName.replace('clicked:', '');
        getAttrs([skill], values => {Code language: JavaScript (javascript)

We start with an array of the button names, and end with grabbing that button name and grabbing its value.

In between, there are several useful functions. seq builds an array from a number so if you have 3, it’ll give [0,1,2] – notice it has 3 elements but starts at 0.

Capitalize and Clicked have already been discussed. If there is anything you find yourself doing a lot, convert them into a function to save yourself some time.

        getAttrs([skill], values => {
            const style = +values[skill] || 0;
            const plot = seq(style +1).join('|');
            const roll_string = `&{template:octane} {{title=${capitalize(skill)} ([[@{${skill}}]])}} {{octane=[[3d6cf0cs>5]]}} ${style ? `{{plot=[[?{Spend Plot Points?|${plot}}d6cf0cs>5]]}}`:''} {{hazard=[[?{enter Hazard|0|1|2|3|4}]]}} {{effect=[[0]]}}`;
            startRoll(roll_string, roll => {Code language: JavaScript (javascript)

Now we are inside getAttrs, we can grab the value of the style in use (stored in a variable called skill). We need to create a string based on the possible number of plot points you can spend, and then build a sting for the roll.

Notice it has keys: title, octane (the standard 3D6 roll), a ternary operator to decide if plot is shown, and a hazard and effect key.

            startRoll(roll_string, roll => {
                const hazard = roll.results.hazard.result;
                const spent_plot = roll.results.hasOwnProperty('plot') ? 
                   roll.results.plot.dice.length : 0;
                const dice = spent_plot ? 
                   [...roll.results.octane.dice, ...roll.results.plot.dice ] : 
                   roll.results.octane.dice;
                const dice_sorted = [...dice].sort(function(a, b){return a-b});
                const score = dice_sorted[dice_sorted.length - hazard -1] || 1;Code language: JavaScript (javascript)

This section does three things (and it’s a lot of code just for this).

First, we get the hazard rating. How many hazards apply on this roll?

Then, we get the displayed dice (for instance 3, 1, 6), but we have to account for extra dice from spent plot points. We want to sort those dice from low to high to make it easier to calculate the final score, but any attempt to sort the dice array tends to sort the original too. That’s why we have […dice].sort – this copies the dice into a new variable, and then sorts them. A simple dice.sort would not work here. It maintains the link to the original, so if you sort the new variable it sorts the original too.

Finally, once the dice are sorted, we can find the highest dice. it’ll be the right-most die, reduced by the hazard rating. If you have no dice left, you get a result of 1!

                const effects = ['Moderator Total Control',
                   'Moderator Total Control','Moderator Partial Control',
                   'Player Partial Control','Player Total Control',
                   'Player Total Control'];
                const effect = effects[score -1];
                finishRoll(roll.rollId, {
                    octane: dice,
                    hazard: score,
                    effect: effect,
                    plot: spent_plot
                });Code language: JavaScript (javascript)

To create the 1-6 table, we create an effects array. This will show the output that matches the result. Then we save some computed values – those we later need to display on the roll template.

                if(score > 4 || spent_plot) {
                    const style_new = style - spent_plot;
                    const add = Math.max(0,style_new) + (score === 6 ? 1 : 0);
                    setAttrs({
                        [skill]: style_new + add
                    });
                }Code language: JavaScript (javascript)

Finally, we might need to update the style value. If the final result is 5 or 6, of the player spent style points, the style value might change. And once this is done, we add the correct number of }); brackets to close the worker.

And some people declare CRP is easy! We aren’t even finished yet.

The Roll Template

Creating a rolltemplate to fidplay the final result is childs play compared to the sheet worker. The end result looks like this (and, as ever, extra work could be applied to make it pretty):

<rolltemplate class="sheet-rolltemplate-octane">
    <div class="heading">{{title}}</div>
    <div class="results">
        {{#rollGreater() computed::plot 0}}
            <div class="key">Spent Plot</div>
            <div class="value">{{computed::plot}}</div>
        {{/rollGreater() computed::plot 0}}
        <div class="key">Roll</div>
        <div class="value"><span class="dice">
            {{computed::octane}}</span> 
        </div>
        {{#rollGreater() hazard 0}}
            <div class="key">Hazard</div>
            <div class="value"><span class="dice">Hazard: {{hazard}}</div>
        {{/rollGreater() hazard 0}}
        <div class="key">Result</div>
        <div class="value">{{computed::hazard}}</div>
        <div class="key">Effect</div>
        <div class="value">{{computed::effect}}</div>
    </div>
</rolltemplate>Code language: HTML, XML (xml)

One of the tricky parts with CRP is keeping straight which computed values you are using for what. Some comments would be handy here. I’ll have to remember to add those in the future!

.sheet-rolltemplate-octane {
    background: white;
    border-radius: 5%;
    margin-left: -40px;
    margin-right: -10px;
}
.sheet-rolltemplate-octane .sheet-heading {
    background: black;
    color: white;
    text-align: center;
}
.sheet-rolltemplate-octane .sheet-results {
    display: grid;
    grid-template-columns: 70px 1fr; 
    column-gap: 2%;
    line-height: 1.8em;
}
.sheet-rolltemplate-octane .sheet-results .sheet-key {
    text-align: right;
}
.sheet-rolltemplate-octane .sheet-dice {
    letter-spacing: 2px;
}
.sheet-rolltemplate-octane .sheet-value span {
    max-width: 185px;
    display: inline-block;
}Code language: CSS (css)

If you have followed earlier posts, the design and styling of this worker won’t be surprising. The main thing is the margin-left and margin-right at the start. Roll20 has very big unused spaces at the left and right of a printed rolltemplate – this eliminates them.

Also, the very final declaration block is to ensure that if you have a LOT of dice being rolled, they overflow on to the next line instead of vanishing invisibly off the screen. For example:

Concluding Words

This was a lot more complex than I expected. As promised, here is the complete sheet worker.

    const styles = ['daring', 'ingenuity', 'craft', 'cunning', 'might', 'magic'];
    const seq = (how_many, start = 0) => 
       [ ...Array(how_many).keys() ].map( increment => increment + start);
    const capitalize = word => word[0].toUpperCase() + word.slice(1);
    const clicked = buttons => buttons.map(button => `clicked:${button}`).join(' ');
    on(clicked(styles), event_info => {
        const skill = event_info.triggerName.replace('clicked:', '');
        getAttrs([skill], values => {
            const style = +values[skill] || 0;
            const plot = seq(style +1).join('|');
            const roll_string = `&{template:octane} {{title=${capitalize(skill)} ([[@{${skill}}]])}} {{octane=[[3d6cf0cs>5]]}} ${style ? `{{plot=[[?{Spend Plot Points?|${plot}}d6cf0cs>5]]}}`:''} {{hazard=[[?{enter Hazard|0|1|2|3|4}]]}} {{effect=[[0]]}}`;
            startRoll(roll_string, roll => {
                const hazard = roll.results.hazard.result;
                const spent_plot = roll.results.hasOwnProperty('plot') ? 
                   roll.results.plot.dice.length : 0;
                const dice = spent_plot ? 
                   [...roll.results.octane.dice, ...roll.results.plot.dice ] : 
                   roll.results.octane.dice;
                const dice_sorted = [...dice].sort(function(a, b){return a-b});
                const score = dice_sorted[dice_sorted.length - hazard -1] || 1;
                const effects = ['Moderator Total Control',
                   'Moderator Total Control','Moderator Partial Control',
                   'Player Partial Control','Player Total Control',
                   'Player Total Control'];
                const effect = effects[score -1];
                finishRoll(roll.rollId, {
                    octane: dice,
                    hazard: score,
                    effect: effect,
                    plot: spent_plot
                });
                if(score > 4 || spent_plot) {
                    const style_new = style - spent_plot;
                    const add = Math.max(0,style_new) + (score === 6 ? 1 : 0);
                    setAttrs({
                        [skill]: style_new + add
                    });
                }
            });
        });
    });Code language: JavaScript (javascript)
Series Navigation<< CRP: Criticals on Multiple Dice (HERO and GURPS)CRP: Essence 20 >>

Leave a Reply

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