Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Adventures with startRoll()

August 30 (3 years ago)
Oosh
Sheet Author
API Scripter

I figured it was time to get a thread going on the new Custom Roll Parsing and see what people have come up with so far. The examples below are probably not beginner-friendly, but the posts are already long enough without going into the workings of setAttrs, getAttrs and so forth - they've been omitted from the code with the assumption that anyone interested in putting startRoll() through its paces will know how to use the basic sheetworkers.

The new startRoll() function, combined with action buttons, offers a bunch of new functionality to explore, as a roll sent to chat can now have custom functions run before it, during it, after it, and even passed through to the next roll. The startRoll() function itself is the main meat of the recent-ish Custom Roll Parsing update, and it's also the first time Roll20 have exposed a Promisified, async/await-ready function for sheet authors. The async/await pattern is great for keeping code tidy & controlled by avoiding callback hell.

To really take advantage of startRoll's promisified version, we need our other major sheetworkers to return Promises. Luckily, OnyxRing got this figured out already - here's the original thread.

I've been using a condensed version, but it's functionally the same as OR's original, and takes the same parameters as Roll20's base functions (obviously):

const asw = (() => {
    const setActiveCharacterId = function(charId){
        let oldAcid=getActiveCharacterId();
        let ev = new CustomEvent("message");
        ev.data={"id":"0""type":"setActiveCharacter""data":charId};
        self.dispatchEvent(ev);
        return oldAcid;
    };
    const promisifyWorker = (workerparameters=> {
        let acid=getActiveCharacterId(); 
        let prevAcid=null;               
        return new Promise((res,rej)=>{
            prevAcid=setActiveCharacterId(acid);  
            try {if (worker===0getAttrs(parameters[0]||[],(v)=>res(v));
                else if (worker===1setAttrs(parameters[0]||{}, parameters[1]||{},(v)=>res(v));
                else if (worker===2getSectionIDs(parameters[0]||'',(v)=>res(v));
            } catch(err) {rej(console.error(err))}
        }).finally(()=>setActiveCharacterId(prevAcid));
    }
    return {
        getAttrs(attrArray) {return promisifyWorker(0, [attrArray])},
        setAttrs(attrObjoptions) {return promisifyWorker(1, [attrObjoptions])},
        getSectionIDs(section) {return promisifyWorker(2, [section])},
        setActiveCharacterId,
    }
})();


Your getAttrs call would then look like this:

let attrs = await asw.getAttrs(['attr1', 'attr2']);

That's it. As long as you don't forget the word 'await', you can now access those attributes with attrs.attr1, attrs.attr2, and so on.

I won't dwell on the async coding part - but all the examples I provide use the above framework. Synchronous versions of all the examples are certainly possible, but.... yeah, probably not alot of fun if you're dealing with a complex, multi-part roll function.

We can also assume that each of these functions is running from a type="action" button and its associated event handler, e.g.

<button type="action" name="act_test">Uninstall Windows</button>

on('clicked:test', (ev) => testFunction(ev));


So, provided you have the usual requirements - minimal HTML to provide some test buttons, a legal script block, and an event listener to attach each main function to your HTML test button - all of the following examples should work copy & pasted into a sheet. They're not terribly useful by themselves, but you should be able to see them working.


Also, something I find generally helpful for reading posts with code on Roll20, a quick Stylus addition:

.postcontent pre {
white-space:pre-wrap!important;
}
.postcontent div {
white-space:pre-wrap;
word-break: break-word;
}

This should wrap code-boxes, and force VS Code lines to wrap when they hit the edge of the black background.


1. Let's Go Fishing - pre-roll data grab

Trick number 1 is pretty straight-forward. Sometimes you might want to fish for some data before starting your roll. A couple of examples:

1. You might want to set up your roll template based on input from the Player. Maybe you want to Query the player on which weapon to use for the attack?

2. You could put a generic button on your damage template, so anyone can click it to apply the damage to their selected token.


All we do is send an API roll to chat (assuming you don't want it to be visible) and use startRoll's functionality to pinch the data from the roll object:

// Weapon rep sec suffixes
const weaponAttributes = ['name''damage''damagetype'];

// Helper function to grab player input
const getQuery = async (queryText=> {
    const rxGrab = /^0\[(.*)\]\s*$/;
    let rollBase = `! {{query1=[[ 0[${queryText}] ]]}}`// just a [[0]] roll with an inline tag
        queryRoll = await startRoll(rollBase),
        queryResponse = (queryRoll.results.query1.expression.match(rxGrab) || [])[1]; 
    finishRoll(queryRoll.rollId); // you can just let this time out if you want - we're done with it
    return queryResponse;
};

// Fake function - the real one would asw.getAttrs() the repeating section and get the rowIds and names
const getWeaponIds = async () => {
    return `weaponName1,weaponId1|weaponName2,weaponId2`;
}

const myRoll = async () => {
    let weaponData = await getWeaponIds(),
        readyWeapon = await getQuery(`?{Which weapon?|${weaponData}}`),
        weapRow = `repeating_weapon_${readyWeapon}`// convenience string for the required row
    // The player has chosen their Weapon by name, and getQuery has returned the rowId we need
    // Run the usual transform on your attribute list to get the attributes needed for the roll
    let requiredAttrs = weaponAttributes.map(a => `${weapRow}_${a}`),
        weapAttrs = await asw.getAttrs(requiredAttrs),
        rollBase = `&{template:default} {{name=Attrs list}} {{=${requiredAttrs.join('\n')}}}`;
    let attackRoll = await startRoll(rollBase);
    // Proceed with the roll, and whatever calculations need doing
    finishRoll(attackRoll.rollId);
}


Not a terribly amazing example as it's so simple - you could achieve the outcome with a simple roll button, but hopefully it gets across the concept of how to pinch text input from a roll.


A second example, making better use of our new functions. Let's say we've got a "Heal Target" action button on the character sheet - we can now coax the heal roll to actually apply the HP to the target. This is probably not ideal sheet design, allowing a player to adjust the HP on another sheet - a better approach would probably be to whisper a second button to the target allowing them to accept the heal and apply the adjustment. But we'll stick with the automatic approach for this example.

Like before, we use a function to grab the data we need with an invisible API roll, then use that info for our actual roll. The difference here is we use OnyxRing's clever function to switch the active character in the sandbox so we can apply the HP change to the target:

// Helper - like getQuery() above, but fishes for a target click
const getTarget = async () => {
    const rxGrab = /^0\[(.*)\]\s*$/;
    let rollBase = `! {{charname=[[0[@{target|h1|character_name}] ]]}} {{charid=[[0[@{target|h1|character_id}] ]]}}}}`,
        targetRoll = await startRoll(rollBase),
        target = {
            name: targetRoll.results.charname.expression.match(rxGrab)[1],
            id: targetRoll.results.charid.expression.match(rxGrab)[1],
        };
    finishRoll(targetRoll.rollId);
    return target;
}

// Main roll function
const healTarget = async () => {
    let target = await getTarget(),
        healerAttrs = await asw.getAttrs(['heal_bonus''character_name']);
    let rollBase = `&{template:default} {{name=Heal}} {{Heal=${healerAttrs.character_name} heals ${target.name} for [[2d6 + ${healerAttrs.heal_bonus || 0}]]}}`;
    let healRoll = await startRoll(rollBase),
        healAmount = healRoll.results.Heal.result;
    // Now we switch the active character in the Sandbox using OnyxRing's function
    console.log(`Switching ID in sandbox to ${target.id}...`);
    asw.setActiveCharacterId(target.id);
    let targetAttrs = await asw.getAttrs(['stamina''stamina_max']),
        finalHp = Math.min(parseInt(targetAttrs.stamina) + healAmountparseInt(targetAttrs.stamina_max));
    setAttrs({stamina: finalHp});
    finishRoll(healRoll.rollId);
}


August 30 (3 years ago)

Edited August 30 (3 years ago)
Oosh
Sheet Author
API Scripter
Package Delivery! - passing roll data through an action button in chat


This trick enables the passing of data from one roll to another via action buttons inserted into roll templates. This functionality was introduced with Custom Roll Parsing alongside startRoll(). Here's an animated example:


This trick involves significantly more setup than the previous trick. First, the button for the second roll needs to exist on the sheet before it can be used from a roll template in chat. We'll hide it on the sheet, since it's only relevant as a reaction to the initial Attack roll being sent to chat:

    <div class="hidden-section" style="display:none">
        <button type="action" name="act_reactroll"></button>
    </div>


Next, we need a custom roll template that enables the use of computed results and assembles an action button for us. This template ain't pretty!

<rolltemplate class="sheet-rolltemplate-mytemp">
    <div class="sheet-outer" style="background-color:white; border:2px solid black;">
        {{#name}}
        <div class="sheet-header" style="font-size:1.2em; font-weight:bold; background-color:lightgrey;">
            {{name}}
        </div>
        {{/name}}
        {{#roll1}}
        <div class="sheet-roll" style="border: 1px solid black">
            {{roll1name}}{{roll1}}
        </div>
        {{/roll1}}
        {{#previousroll}}
        <div class="sheet-roll" style="border: 1px solid black">
            {{previousrollname}}{{previousroll}}
        </div>
        {{/previousroll}}
        {{#showoutcome}}
        <div class="sheet-outcome" style="color: blue; background-color: whitesmoke;">
            {{computed::outcome}}
        </div>
        {{/showoutcome}}
        {{#^rollTotal() computed::passthroughdata 0}}
        <div class="sheet-button">
            [{{buttonlabel}}](~selected|{{buttonlink}}||{{computed::passthroughdata}})
        </div>
        {{/^rollTotal() computed::passthroughdata 0}}
    </div>
</rolltemplate>

Worth noting that the target for the action button here is hard-coded to ~selected. You could also use the data grab method from example 1 to, for example, prompt the Attacking player for their target before the attack roll, and use that to whisper the reactive Defender roll to the target of the attack. Trick 1 shows how to grab the target id (for the action button) and the target name (for the whisper).

Finally, here's a big chunk of Javascript. Apologies for the roll template text running off the post - use the CSS trick at the top to get the wrapping working!

// Helper - the data passed through in an action button needs these characters escaped
// I've used a mongrel escape sequence to avoid triggering anything internal in Roll20
// This may not be a complete list of characters that need escaping! If you have heavily
// punctuated text, it might break the passthrough!
const rollEscape = {
    chars: {
        '"': '%quot;',
        ',': '%comma;',
        ':': '%colon;',
        '}': '%rcub;',
        '{': '%lcub;',
    },
    escape(str) {
        str = (typeof(str) === 'object') ? JSON.stringify(str) : (typeof(str) === 'string') ? str : null;
        return (str) ? `${str}`.replace(new RegExp(`[${Object.keys(this.chars)}]`'g'), (r=> this.chars[r]) : null;
    },
    unescape(str) {
        str = `${str}`.replace(new RegExp(`(${Object.values(this.chars).join('|')})`'g'), (r=> Object.entries(this.chars).find(e=>e[1]===r)[0]);
        return JSON.parse(str);
    }
}

// Primary Roll, triggered from sheet as usual
const rollAttack = async () => {
    // We'll pretend we've done a getAttrs on the attacker's weapon for all the required values
    // Row ID's must be provided when using action buttons too, we'll skip all of that here though
    let attrs = {
        character_name: 'Alice',
        weapon_name: 'Sword',
        attack_bonus: '5',
    }
    let rollBase = `&{template:mytemp} {{name=${attrs.character_name} Attack}} {{roll1name=${attrs.weapon_name}}} {{roll1=[[1d20 + (${attrs.attack_bonus})]]}} {{passthroughdata=[[0]]}} {{buttonlabel=Next Roll}} {{buttonlink=reactroll}}`;
    let attackRoll = await startRoll(rollBase),
        roll1Value = attackRoll.results.roll1.result;
    // Storing all the passthrough data required for the next roll in an Object helps for larger rolls
    let rollData = {
        attacker: attrs.character_name,
        attackTotal: roll1Value,
    }
    // Finish the roll, passing the escaped rollData object into the template as computed::passthroughdata
    // Our roll template then inserts that into [butonlabel](~selected|buttonlink||<computed::passthroughdata>)
    // ~selected allows anyone to click the button with their token selected. Omitting this will cause the button
    // to default to whichever character is active in the sandbox when the button is created
    finishRoll(attackRoll.rollId, {
        passthroughdata: rollEscape.escape(rollData),
    });
};

// The defend roll triggered from the button sent to chat by rollAttack()
const rollReact = async (ev=> {
    // The data we passed into the button will be stored in the originalRollId key
    let attackRoll = rollEscape.unescape(ev.originalRollId);
    console.info(attackRoll);
    // Another fake getAttrs() return here with the Defender's attributes
    let attrs = {
        character_name: 'Bob',
        weapon_name: 'Celery',
        attack_bonus: '-10',
    }
    let rollBase = `&{template:mytemp} {{name=${attrs.character_name} Defend}} {{roll1name=${attrs.weapon_name}}} {{roll1=[[1d20 + (${attrs.attack_bonus})]]}} {{previousrollname=${attackRoll.attacker}'s Attack}} {{previousroll=${attackRoll.attackTotal}}} {{showoutcome=1}} {{outcome=[[0]]}}`;
    let defendRoll = await startRoll(rollBase);
    // Now we can do some further computation to insert into the {{outcome}} field, primed with a [[0]] roll
    let defendTotal = defendRoll.results.roll1.result;
    let resultText = (defendTotal >= attackRoll.attackTotal
        ? `${attrs.character_name} defends successfully!`
        : `${attackRoll.attacker} attacks successfully!`;
    // Finish the roll, inserting our computed text string into the roll template
    finishRoll(defendRoll.rollId, {
        outcome: resultText,
    });
}

// The reactroll button still needs its event listener, just like a normal button
on('clicked:reactroll'async (ev=> {
    console.log(`Starting react roll`);
    await rollReact(ev);
    console.log(`Completed react roll`);
});


I won't go into detail about the CSS, but as a general tip if you're using startRoll() to compute text strings you'll probably want to disable the Quantum Roll tooltip, as it only ever contains the original roll result (in this case, just a placeholder 0 roll, so the tooltip would say "Rolling 0 = 0"). A quick and easy way to disable it is to include a pointer-events: none; line in your CSS for the .inlinerollresult class in your text section.

Important: The event listener for a button which is receiving passthrough data, must pass the event object to your roll function. This is where Roll20 stash the originalRollId which we've hijacked.

August 30 (3 years ago)

Edited September 01 (3 years ago)
Oosh
Sheet Author
API Scripter

3. Multiattack - sending multiple templates as one


This trick comes in handy in a situation where you need a roll to influence a second roll, but you want the Quantum Roll tooltip to be intact and accurate. With the normal use of startRoll(), your computed results use the tooltip for the original roll. This isn't always a problem - if your roll isn't overly complicated, you can usually get where you need to by showing the working, using roll template tricks to hide irrelevant rolls, and using computed results for final results & display output rather than the full roll expression.

If you don't care about the Quantum Roll tooltip, you can also just do sub-rolls, or less vital rolls, using Javascript's Math.random().

But if you do want accurate, honest Quantum tooltips, and you have a second roll which relies on the result of the first, using a second startRoll() and some CSS wizardry can get you there.

First, we've got a roll template. It's mostly straight-forward and basic - the unusual things about it are the top & bottom spacers, and the extra classname inserted into the body if the footer is present:

<rolltemplate class="sheet-rolltemplate-stacktemp">
    {{#title}}
    <div class="sheet-header-spacer"></div>
    <div class="sheet-title" style="font-size:1.2em; background-color:black; color: white;">
        {{title}}
    </div>
    {{/title}}
    <div class="sheet-body{{#footer}} sheet-bottom{{/footer}}" style="border:1px solid black; padding: 3% 1% 3% 1%;background-color: white;">
        {{description}}
    </div>
    {{#footer}}
    <div class="sheet-footer" style="font-style: italic; background-color:black; color: white;">
        {{footer}}
    </div>
    <div class="sheet-footer-spacer"></div>
    {{/footer}}
</rolltemplate>


Having the entire top or bottom section as optional allows us to use {{title}} + {{description}} for the top half, then {{description}} + {{footer}} for the bottom half.

The Javascript:

// A data object holding our crit table/effect table/whatever
const dataTable = {
    1: `You find [[2d4]] pieces of lint in your belly-button.`,
    2: `It's been [[2d6]] weeks since you called your mother. You are cursed.`,
    3: `Wowbagger the Infinitely Prolonged insults you for [[3d10]] sanity damage.`,
    4: `You've been reading this post for [[2d10]] minutes. It's time to take a break.`
}

// The main roll function
const doubleRoll = async () => {
    // Start with the first roll & top half of the multi-template
    let topRollBase = `&{template:stacktemp} {{title=Something Happens!}} {{description=Event die: [[1d4]]}}`;
    let topRoll = await startRoll(topRollBase),
        eventResult = topRoll.results.description.result,
        eventText = dataTable[eventResult];
    // Now we move on to the bottom half - *without* finishRoll()'ing the first roll, of course!
    // We want all the dice to be rolled & computed before we get to finishRoll to prevent lag between parts
    let bottomRollBase = `&{template:stacktemp} {{description=${eventText}}} {{footer=Unlucky Alf}}`;
    let bottomRoll = await startRoll(bottomRollBase);
    finishRoll(topRoll.rollId);
    finishRoll(bottomRoll.rollId);
}


This is a pretty basic roll. The [[1d4]] determines which result from the table we use for the second part. The inline rolls in the text will then be parsed when we send the bottom half to chat, and both halves are original, non-computed rolls so they enjoy unsullied Quantum deliciousness. The main point here is to await startRoll() both of our rolls before we get to finishRoll()'ing anything - otherwise your template parts will be spat out with lag in between while the rolls are sent to the Quantum server.

Now, we just need a little CSS help to nudge the template parts together. The space Roll20 leaves between templates is 9px (I believe it's always 9, but never used the site on mobile). A negative margin will push our templates up together, but it will push all the {stacktemp} templates closer, which isn't necessarily what we want. This is why we've got the two empty "sheet-header/footer-spacer" divs, which only display when the relevant {{title}} or {{footer}} is present. We apply a height to these, so when a template has a {{title}}, the negative top margin is offset by the header spacer and ends up at a normal distance from the post above it. Same story for the footer & bottom spacer.

Depending on your template layout & other margins, you may want to split the 9px between the top and bottom. Just set your top/bottom spacers to the same height as the matching negative margin, and your templates should end up in the right place when they're not being used as a sandwich.

So here's the CSS:

.sheet-rolltemplate-stacktemp {
    margin-7px 5% -2px -5%;
    text-align:center;
}
.sheet-rolltemplate-stacktemp .sheet-header-spacer {
    min-height7px;
}
.sheet-rolltemplate-stacktemp .sheet-footer-spacer {
    min-height2px;
}


Now, there's one other issue with this method - after 6 posts to chat, Roll20 automatically inserts a playerName break & chat avatar.So if you post a double template after 5 uninterrupted posts, Roll20 delivers a combo-breaker and makes a mockery of our negative margins. The first template set here is normal, the second template set is cut in half by Roll20 eagerly letting us know our own name:


edit - the solution below doesn't factor in long player/character names taking up multiple lines, see Scott's response below

Credit to Scott C for the CSS solution here. When Roll20 inserts that player name, it inserts it at the start of the post. We can exploit this by assuming any template that is *not* the first-child, has had a break inserted before it. But we're not quite there! We *only* want the bottom parts of templates to be dragged up to meet the previous one. We don't want a fresh, top-half template with a title to be dragged up to the previous post. That's where we use the extra class "sheet-bottom" inserted into our roll template when a footer is supplied. This gives us the following CSS rules:

/* Chat avatar fix */
.sheet-rolltemplate-stacktemp {
    position:relative;
}
.sheet-rolltemplate-stacktemp:not(:first-child.sheet-body.sheet-bottom {
    margin-top-32px;
}

The position: relative; line is enough to place the template on top of the avatar & text - no z-indexing is required.

So now we have a working double-template:

The first example is normal - taking up 2 posts in the normal 6-post cycle. The second example shows a split template, with the bottom half dragged up into the previous post (you can play around with top & bottom margins here to get the join right on the chat-border if this is triggering you :). And the third double-template shows how the extra conditional class name in the roll template prevents a fresh template from getting the negative margin applied, dragging it north over the border.

August 30 (3 years ago)

Edited August 30 (3 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

Nice write-up Oosh. I hadn't thought of dragging the extras over the player's avatar text. Unfortunately, it's a method that's extremely fragile. If the player has a name that's too long and wraps, your spacing won't work. and you wind up with the templates not lining up appropriately and some of the name being displayed:

I think the better way (although certainly not as stylish) is to take the technique and style the top border and offset of the secondary rolls based on whether it is a first-child or not. this gives you output like this:

Which certainly isn't as seemless as your method, but does avoid the odd overlapping for oddly long names.

August 30 (3 years ago)
Andreas J.
Forum Champion
Sheet Author
Translator

Also, something I find generally helpful for reading posts with code on Roll20, a quick Stylus addition:

.postcontent pre {
white-space:pre-wrap!important;
}
.postcontent div {
white-space:pre-wrap;
word-break: break-word;
}

This is btw what I've slowly started to add to all code blocks found on the wiki

August 31 (3 years ago)
.Hell
Sheet Author

Its not clear to me if your tricks use the API or not. Can you elaborate which tricks should work without plus/pro accounts? Thanks

August 31 (3 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

These are all just sheetworker methods using the new roll parsing capability of character sheets. It brings a lot of functionality that was formerly only possible with the API.

August 31 (3 years ago)
Andreas J.
Forum Champion
Sheet Author
Translator

There isn't much info or community docs on this update yet, but there is a start here: https://wiki.roll20.net/Sheetworkers#Roll_Parsing.28NEW.29

September 01 (3 years ago)
Oosh
Sheet Author
API Scripter

.Hell said:

Its not clear to me if your tricks use the API or not. Can you elaborate which tricks should work without plus/pro accounts? Thanks

As Scott said, these are purely sheetworker methods, with the account requirements that come with that:

Pro is required for Custom Sheets/Sheet Sandbox (though technically not required to maintain a sheet, as that is through GitHub).

No paid membership is required to use a sheet with Custom Roll Parsing (or these tricks based on it) once it has been submitted & accepted by Roll20, and appears on the usual Character Sheet selection drop-down.


I'm keen to see what other tricks people come up with!

I know Scott C has been beavering away on some awfully complicated system with Custom Roll Parsing, presumably he has a few aces up his sleeve by now....


September 01 (3 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
I may have a few minor extensions, but more in the vein of "this means you can do x" variety.than real differences from what you've ready got.
September 01 (3 years ago)

Edited September 01 (3 years ago)
Justin V.
Pro
Sheet Author
Translator

Nice Oosh, I'm having my own adventures with startRoll() in the last few days and I really like the functionally it adds.

I've hit a snag though and I'm wondering if people here can help.

I'm trying to call repeating section attributes from the same repeating section as the action button is inside, but to do that from multiple repeating rows i need to replace the rowId for these attributes inside the roll info, I'm failing to be able to do this dynamically. Getting the rowId is the easy enough, but I've not managed with inserting it into the roll information so it is then processed by startRoll correctly.


A static/manual roll from a specific rowId works fine.

In this case it's a skill test action (clicked:repeating_artspecializationr:artspecializationroll), with target calculated from attributes within repeating_artspecializationr_-misdjapu9aczufzlwov_

    on('clicked:repeating_artspecializationr:artspecializationroll', (event) => {		
        startRoll("@{Whisper} &{template:whfrp4e} {{test=[[@{roll_rule}]]}} {{sl=[[0]]}} {{target=[[ [[@{repeating_artspecializationr_-misdjapu9aczufzlwov_ArtSpecializationChar}+@{repeating_artspecializationr_-misdjapu9aczufzlwov_ArtSpecializationAdv}+@{repeating_artspecializationr_-misdjapu9aczufzlwov_ArtSpecializationMisc}]] [SKILL] +?{@{translation_modifier}|0} [MOD] ]]}}", (results) => {
            const target = results.results.target.result
            const test = results.results.test.result
            const sl = results.results.sl.result
            const computed = (Math.floor(target / 10) - Math.floor(test / 10)) ;
            const computed2 = (((test-1)%10)+1) ;
               console.log(target);
               console.log(test);
               console.log(computed);
               console.log(computed2);
            finishRoll(
                results.rollId,
                {
                    test: computed2,
                    sl: computed,
                }
            );
        });
    });
The above worker works fine.

Any ideas on building the pre-startRoll variable's with the clicked rowId populated as needed which will get inserted as the roll information? It should be possible from what i gather above.:)



September 01 (3 years ago)
.Hell
Sheet Author

Hey Justin,


maybe this Thread https://app.roll20.net/forum/post/10298926/calculated-rolls-with-repeatable-rows might help you :)

September 01 (3 years ago)
Justin V.
Pro
Sheet Author
Translator


.Hell said:

Hey Justin,


maybe this Thread https://app.roll20.net/forum/post/10298926/calculated-rolls-with-repeatable-rows might help you :)


Oh indeed, thank you!

September 02 (3 years ago)

Edited September 02 (3 years ago)
Oosh
Sheet Author
API Scripter

As a side note, I ended up almost doing away with Roll20's attribute parsing in my code. For a simple roll, if you don't need to getAttrs anyway, it can still be handy (just inserting @{character_name} into a short description template, for example) - but all my action button rolls need a getAttrs, so I wrote a few fetch functions for different purposes (grab all the attribute for a repeating weapon row, for example) which get returned in an Object with my own notation, ready to be inserted into a template literal to send to startRoll(). So instead of

"@{repeating_weapon_-9dfs89h23h9fh823_weapon_name} hits for... blah blah blah...."

I'd be using

 `${wp.name} hits for [[${wp.damage}]] ${wp.damageType} damage`.

I found it considerably easier to read back my own roll functions to fix errors later - YMMV of course, but if you're getAttrs'ing anyway and you're comfortable with JS object notation + camelCase, I found it much easier on the eyeballs.

As an extra added bonus, if your attribute names need to change later for any reason, you don't need to change your roll expressions - just change the fetch function.

September 02 (3 years ago)
Brad H.
Sheet Author
API Scripter

Awesome set of tricks Oosh, the passing of a payload to another roll is fantastic.

One small trick that others may find useful.  I wanted to find a way to grab text, not number, from a roll.  Example: {{pilot=@{target|Pilot|character_id}}.  However results.results.pilot is undefined because there is no roll being made.  Wrapping in roll brackets does not help either as the roll parser expects numbers and throws an exception.   A workaround is to use the inline labels {pilot=[[0[@{target|Pilot| character_id]]]}} .  The value of the @target or @selected now shows in the result object under the "expression" property.  

example: {{driver=[[0[@{target|Driver|token_name}]]]}} and selecting the token "Bob" will return "0[Bob]".  The name can be extracted using .match.

const myroll="/w gm &{template:info}{{action=Set Driver}}{{driver=[[0[@target|Driver|character_id}]]]}}
startRoll(myroll, (results) => {
    sets["driver"]=results.results.driver.expression.match(/\[(.*)\]/)[1];
    finishRoll( results.rollId);
    setAttrs(sets);
});

I have tested with @selected, @target and @tracker


September 13 (3 years ago)

Edited September 16 (3 years ago)
.Hell
Sheet Author

Hey Oosh,

I try to do your Package Delivery but I dont get the passthrough data linked to the button. It is always empty. Everything else up to this point works. What could be the issue?

{{#^rollTotal() computed::passthroughdata 0}}
<tr><td><div class="sheet-rolltemplate-spacer"> </div></td></tr>
<tr>
<td>
<div class="sheet-button">
[Dodge](~selected|reactdodge||{{computed::passthroughdata}})
</div>
</td>
</tr>
{{/^rollTotal() computed::passthroughdata 0}}



let attackRoll = await startRoll(rollPart + "{{glitch=[[0]]}} {{passthroughdata=[[0]]}}");

var glitchComputed = 0;
var amount = attackRoll.results.result.dice.filter(x => x == 1).length;

if(amount > attackRoll.results.result.dice.length/2) {
glitchComputed = 1;

if(attackRoll.results.result.dice.filter(x => x >= 6).length == 0) {
glitchComputed = 2;
}
}


let attrs = await asw.getAttrs(['character_name']);
// Storing all the passthrough data required for the next roll in an Object helps for larger rolls
let rollData = {
attacker: attrs.character_name,
attackHits: attackRoll.results.result.result,
}

finishRoll(
attackRoll.rollId,
{
glitch: glitchComputed,
passthroughdata: rollData,

}
);
September 17 (3 years ago)

Edited September 17 (3 years ago)
Oosh
Sheet Author
API Scripter

It looks like you've missed a step in the passthrough method - the Object needs to be stringified, and have a bunch of characters escaped before it'll work. It's being inserted into an action button and needs to be a string which doesn't break Roll20's action button parsing.


In the passthrough example up above, at the top is an object called rollEscape with a couple of methods in it - escape() and unescape(). You can just copy paste that, or write your own if you prefer - but you need to at least escape the characters I've got there - there might be others, but those should cover your standard Object.


Once you have that code (or equivalent) available in your workers, you just need to rollEscape.escape(rollData) before you pass it to the finishRoll() function. Your receiving function (activated by clicking the button with rollData attached to it) then needs to rollEscape.unescape(event.originalRollId) to turn the escaped string back into a data Object.


You can't pass actual JS data through this way - it's being unwittingly and unwillingly shoved into an HTML tag, so it absolutely needs to be stringified.


If you're still having issues after making the above changes, try posting back with some console errors, and also the contents of the action button being generated by your roll template - right click on the button and "copy link location", and paste the contents with your reply.

September 17 (3 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

Heh, I went a different direction with passing data from roll to roll. I made a repeating section that was only created by the sheetworkers (not present in the html of the sheet). Then stringified and stored each roll that needed saving in an attribute of the section along with the epoch time that the roll was made.

Then whenever a roll was made that included the originalRollId, I just grab the rolls repeating section contents and grab the data I need. Doing it this way does mean that you need a sheetworker to clean up the roll repeating section though. I limited it to 50 rolls long (way more than you'd ever possibly need) and no more than a day old, while always keeping at least one roll in it.

September 17 (3 years ago)
.Hell
Sheet Author

Hey Oosh,


thanks for your input. Unfortunately it does not work.

let attrs = await asw.getAttrs(['character_name']);
// Storing all the passthrough data required for the next roll in an Object helps for larger rolls
let rollData = {
attacker: attrs.character_name,
attackHits: attackRoll.results.result.result,
}

console.log(rollEscape.escape(rollData));

finishRoll(
attackRoll.rollId,
{
glitch: glitchComputed,
passthroughdata: rollEscape.escape(rollData),

}
);

I copied your rollEscape.

This is the console output. From the escaped strings it look ok, but maybe you see something amiss.

The link adress shows that the data is missing: https://app.roll20.net/editor/~selected|reactdodge||

Thanks for your time helping me out

September 17 (3 years ago)
Oosh
Sheet Author
API Scripter

Hrmmm, I can't see any error there, I'm not really sure why it wouldn't be working. If you just put passthroughdata: 'blahblahblah' in your finishRoll() Object, does it show up in the button link? And is your other computed roll key {{glitch}}, all working properly?

September 17 (3 years ago)
Oosh
Sheet Author
API Scripter


Scott C. said:

Heh, I went a different direction with passing data from roll to roll. I made a repeating section that was only created by the sheetworkers (not present in the html of the sheet). Then stringified and stored each roll that needed saving in an attribute of the section along with the epoch time that the roll was made.

Then whenever a roll was made that included the originalRollId, I just grab the rolls repeating section contents and grab the data I need. Doing it this way does mean that you need a sheetworker to clean up the roll repeating section though. I limited it to 50 rolls long (way more than you'd ever possibly need) and no more than a day old, while always keeping at least one roll in it.


Interesting! I did consider that line of attack, but I needed a global button that anyone can click to launch the second stage of the roll. I couldn't think of a way to get that to work entirely through sheetworkers, since the player launching the second stage didn't roll the first stage, the only way their sheet can know which character sheet the roll data are stored on is by stamping it on the 1st-stage roll template somehow. At that point, I figured if I was hiding a character ID in there, I may as well see just how much data Roll20 would let me stuff in :)

September 17 (3 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

Huh, interesting. And, yeah, in that case the repeating method wouldn't work.

September 17 (3 years ago)
.Hell
Sheet Author


Oosh said:

Hrmmm, I can't see any error there, I'm not really sure why it wouldn't be working. If you just put passthroughdata: 'blahblahblah' in your finishRoll() Object, does it show up in the button link? And is your other computed roll key {{glitch}}, all working properly?


Didnt help, still no data coming through. The glitch works as expected and even if I put the glitch into the passthroughdata nothing is added to the button

September 18 (3 years ago)

Edited September 18 (3 years ago)
Oosh
Sheet Author
API Scripter

Hmmmm what's the contents of "rollPart"? Try swapping the order of the computed parts in your template so passthroughdata comes first. Also maybe chuck a console.log(attackRoll) inside roll somewhere just to make sure the initial passthroughdata roll is properly in the object.

let attackRoll = await startRoll(rollPart + "{{passthroughdata=[[0]]}} {{glitch=[[0]]}}");


It's possible you've hit the roll index bug - you can only compute the first 10 declared rolls in a roll template, as the indexing breaks custom roll parsing once it hits double digits.

September 18 (3 years ago)
.Hell
Sheet Author

Switching it around didnt help, but I think you are on to something. When I dont use the computed value it adds it to the button.The roll itself has 15 entries under results. So following your explanation of the bug the computed::glitch should not work too. But it does :/

September 18 (3 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

Wait, you have 15 rolls in rollPart? That's your problem, since only the 1st 10 rolls sent in the template can be displayed as computed values. Put your passthroughData field as the first field and it should work.

September 19 (3 years ago)

Edited September 19 (3 years ago)
Oosh
Sheet Author
API Scripter

Yep, what Scott said - you can have as many rolls as you want in the template, but only the first 10 [[rolls]] (in order of declaration) will work with ::computed. You might need to remove your template declaration from rollPart, then rearrange it as:

let attackRoll = await startRoll("&{template:myTemplate} {{passthroughdata=[[0]]}} {{glitch=[[0]]}}" + rollPart);


I'm not sure why {{glitch}} is currently working - it should be subject to the bug if it's after roll number 10.

September 19 (3 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

I'll also note that it's only the first 10 fields that can be computed, not just the first 10 rolls. So things like {{character_name=@{character_name}}} take up a computation "slot", at least until this bug gets fixed.

September 19 (3 years ago)
Oosh
Sheet Author
API Scripter

Oh, really? That's odd - I thought it was a bad regex or something grabbing the index with \$\[\[(\d), and missing the + to allow more than one digit. But if every handlebar is counting towards the 10 I guess that's not it.

September 19 (3 years ago)
.Hell
Sheet Author


Scott C. said:

Wait, you have 15 rolls in rollPart? That's your problem, since only the 1st 10 rolls sent in the template can be displayed as computed values. Put your passthroughData field as the first field and it should work.


Thanks that did the trick! :)

October 20 (3 years ago)
Richard M.
Pro
Sheet Author

I'm still something of a novice with sheet creation, so hoping someone can give me a bit of a steer.

I'm working on a sheet for a system (The Troubleshooters) that has a somewhat byzantine logic for calculating initiative from a dice roll. Using custom roll parsing, I've managed to create a sheetworker to calculate and output the right end value, but does anyone know if there's a way to send a computed result to the tracker, either within the sheetworker or the roll template? I've tried adding &{tracker} at various likely looking points, without success and it's all just trial and error (mostly error) at the moment.

Grateful for any advice...

October 20 (3 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

Hmm, I hadn't tried computed initiatives yet. It looks like that isn't directly possible. Here's how I'd work around this:

on('clicked:initiative',async (event)=>{
	const displayRoll = await startRoll('&{template:compute} {{init=[[1d20]]}}');
	// Do your calculations and computations as needed. Store the final initiative result in the below variable
	let trueInitiative = displayRoll.results.init.result*5.5/displayRoll.results.init.result;//some random math to demonstrate
	finishRoll(displayRoll.rollId,{init:trueInitiative});//Finish of the display roll so that the roll is displayed to the players
	const trackerRoll = await startRoll(`![[${trueInitiative}&{tracker}]]`);//Now send the calculated roll to the tracker. Doing it as an API command by starting it with '!' will make it invisible to the players
	finishRoll(trackerRoll.rollId);//finish the tracker roll immediately since we aren't going to do any manipulation of it
});
October 20 (3 years ago)
Richard M.
Pro
Sheet Author

Thanks Scott, that's worked a treat.

Much obliged