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 .
×
Advertisement Create a free account

Adventures with startRoll()

1630330925
Oosh
Sheet Author
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  = ( worker ,  parameters )  =>  {          let   acid = getActiveCharacterId ();           let   prevAcid = null ;                         return   new   Promise (( res , rej ) => {              prevAcid = setActiveCharacterId ( acid );                try  { if  ( worker === 0 )  getAttrs ( parameters [ 0 ]||[],( v ) => res ( v ));                  else   if  ( worker === 1 )  setAttrs ( parameters [ 0 ]||{},  parameters [ 1 ]||{},( v ) => res ( v ));                  else   if  ( worker === 2 )  getSectionIDs ( parameters [ 0 ]|| '' ,( v ) => res ( v ));             }  catch ( err ) { rej ( console . error ( err ))}         }). finally (() => setActiveCharacterId ( prevAcid ));     }      return  {          getAttrs ( attrArray ) { return   promisifyWorker ( 0 , [ attrArray ])},          setAttrs ( attrObj ,  options ) { return   promisifyWorker ( 1 , [ attrObj ,  options ])},          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 ) +  healAmount ,  parseInt ( targetAttrs . stamina_max ));      setAttrs ({ stamina :   finalHp });      finishRoll ( healRoll . rollId ); }
1630330959

Edited 1630331099
Oosh
Sheet Author
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.
1630330980

Edited 1630468247
Oosh
Sheet Author
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-height :  7px ; } .sheet-rolltemplate-stacktemp   .sheet-footer-spacer  {      min-height :  2px ; } 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.
1630348077

Edited 1630348230
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.
1630355721
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
1630417962
.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
1630421589
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.
1630421922
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: <a href="https://wiki.roll20.net/Sheetworkers#Roll_Parsing.28NEW.29" rel="nofollow">https://wiki.roll20.net/Sheetworkers#Roll_Parsing.28NEW.29</a>
1630468835
Oosh
Sheet Author
.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 &amp; 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....
1630469282
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.
1630508875

Edited 1630515745
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&nbsp; rowId works fine. In this case it's a skill test action&nbsp; (clicked:repeating_artspecializationr:artspecializationroll) , with target calculated from attributes within repeating_artspecializationr_-misdjapu9aczufzlwov_ &nbsp; &nbsp; on('clicked:repeating_artspecializationr:artspecializationroll', (event) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; startRoll("@{Whisper} &amp;{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) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const target = results.results.target.result &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const test = results.results.test.result &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const sl = results.results.sl.result &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const computed = (Math.floor(target / 10) - Math.floor(test / 10)) ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const computed2 = (((test-1)%10)+1) ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;console.log(target); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;console.log(test); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;console.log(computed); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;console.log(computed2); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; finishRoll( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; results.rollId, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; test: computed2, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sl: computed, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; }); 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.:)
1630518086
.Hell
Sheet Author
Hey Justin, maybe this Thread <a href="https://app.roll20.net/forum/post/10298926/calculated-rolls-with-repeatable-rows" rel="nofollow">https://app.roll20.net/forum/post/10298926/calculated-rolls-with-repeatable-rows</a> might help you :)
1630523645
Justin V.
Pro
Sheet Author
Translator
.Hell said: Hey Justin, maybe this Thread <a href="https://app.roll20.net/forum/post/10298926/calculated-rolls-with-repeatable-rows" rel="nofollow">https://app.roll20.net/forum/post/10298926/calculated-rolls-with-repeatable-rows</a> might help you :) Oh indeed, thank you!
1630554520

Edited 1630554612
Oosh
Sheet Author
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.
1630617446
Brad H.
Pro
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.&nbsp; I wanted to find a way to grab text, not number, from a roll.&nbsp; Example: {{pilot=@{target|Pilot|character_id}}.&nbsp; However results.results.pilot is undefined because there is no roll being made.&nbsp; Wrapping in roll brackets does not help either as the roll parser expects numbers and throws an exception.&nbsp; &nbsp;A workaround is to use the inline labels {pilot=[[0[@{target|Pilot|&nbsp;character_id]]]}} .&nbsp; The value of the @target or @selected now shows in the result object under the "expression" property.&nbsp;&nbsp; example: {{driver=[[0[@{target|Driver|token_name}]]]}} and selecting the token "Bob" will return "0[Bob]".&nbsp; The name can be extracted using .match. const myroll="/w gm &amp;{template:info}{{action=Set Driver}}{{driver=[[0[@target|Driver|character_id}]]]}} startRoll(myroll, (results) =&gt; { &nbsp; &nbsp; sets["driver"]=results.results.driver.expression.match(/\[(.*)\]/)[1]; &nbsp; &nbsp; finishRoll( results.rollId); &nbsp; &nbsp; setAttrs(sets); }); I have tested with @selected, @target and @tracker
1631560227

Edited 1631798603
.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}} &lt;tr&gt;&lt;td&gt;&lt;div class ="sheet-rolltemplate-spacer" &gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt; &lt;tr&gt; &lt;td&gt; &lt;div class ="sheet-button" &gt; [Dodge](~selected|reactdodge||{{computed::passthroughdata}}) &lt;/div&gt; &lt;/td&gt; &lt;/tr&gt; {{/^rollTotal() computed::passthroughdata 0}} let attackRoll = await startRoll(rollPart + "{{glitch=[[0]]}} {{passthroughdata=[[0]]}}"); var glitchComputed = 0; var amount = attackRoll.results.result.dice.filter(x =&gt; x == 1).length; if(amount &gt; attackRoll.results.result.dice.length/2) { glitchComputed = 1; if(attackRoll.results.result.dice.filter(x =&gt; x &gt;= 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, } );
1631853267

Edited 1631853526
Oosh
Sheet Author
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.
1631855670
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.
1631856597
.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: <a href="https://app.roll20.net/editor/~selected|reactdodge||" rel="nofollow">https://app.roll20.net/editor/~selected|reactdodge||</a> Thanks for your time helping me out
1631864321
Oosh
Sheet Author
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?
1631864659
Oosh
Sheet Author
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 :)
1631885593
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Huh, interesting. And, yeah, in that case the repeating method wouldn't work.
1631889224
.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
1631924060

Edited 1631924894
Oosh
Sheet Author
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.
1631965790
.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 :/
1631993662
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.
1632010543

Edited 1632010589
Oosh
Sheet Author
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("&amp;{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.
1632010759
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.
1632046775
Oosh
Sheet Author
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.
1632059401
.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! :)
1634730574
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 &amp;{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...
1634743002
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)=&gt;{ const displayRoll = await startRoll('&amp;{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}&amp;{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 });
1634754396
Richard M.
Pro
Sheet Author
Thanks Scott, that's worked a treat. Much obliged