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

Storing a query value in an Attribute

Hi! I'm not gonna say that I've been through everything already since being referred to Roll Parsing just last night/this morning but I have done a fair bit of reading and can't help but feel that I just don't have a good enough understanding of what the Roll20 site/engine does.  I have read through Oosh's wonderful  Adventures with StartRoll()  and while I can grasp some of it I don't know why certain things are utilized.  I found this pretty quickly: // 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 ; }; Which looks like just about what I need but honestly don't know how to apply it in my setting. const rxGrab: I don't really get what the need for this regex is?  It's looking for 0[something] is that due to the structure of something Roll20 has done? ?{ } is the basic setup for a query, with | being used to set a value and other factors such as creating a list.  I don't need all that extra just to take the value from ?{PlayerInput} and store it in attr_PsyUsed or some such. Is there not a simple way to grab that value?  It doesn't matter if it is overwritten each time the ToHit Roll is triggered because I only need the most recent value that is entered by the player. In another thread I found I asked about this as: Dustin S.  said: Sorry to hijack/necro this but I imagine this is something very simple that I'm just not wrapping my head around. I am looking to effectively just do this: Scott C.  said: This would require that you create an attribute to store the query in and reference it in the macro like so: {{Dodge=[[d20+@{dodge} - @{dodge_query}]]}} I want to create an attribute to store the value of a query from a longer calculation/macro.  I've only used a bit of API and haven't got a firm grasp of the roll20 wizardry. <div class="sheet-item sheet-redbgns" style="width: 15%;"><button name="roll_PsyHit" type="roll" value="!roll40k @{character_name}, ?{Focus Power Characteristic | Willpower,@{Willpower} | Perception,@{Perception} | Psyniscience,@{PsyniscienceCharacteristic}}, [[(@{PsyRating} - ?{Psy Use?|1}) *10 + ?{Modifier|0}]]"> <span data-i18n="hit-u">Hit</span> </button></div> This is the button for the roll and I want to save the used PsyRating into an attribute so that when I perform the damage roll that can be added if necessary, also to add some flavor text in the result of the roll. EDIT: I guess I should add that I'm editing/fixing an existing sheet and am trying to go for the least impact possible.  I might have to completely rewrite the existing API if there's not a way to pull out this singular query and store it in an attribute. Thanks.
1634785817

Edited 1634791533
Oosh
Sheet Author
API Scripter
Hi Dustin, There are plenty of ways to code something, and this example is just one way of doing it. There's probably a different approach that would make more sense to you, and the senior sheet authors around the place probably have better methods than mine. But to address your main points: Is there not a simple way to grab that value?  It doesn't matter if it is overwritten each time the ToHit Roll is triggered because I only need the most recent value that is entered by the player. Not that I'm aware of. You can grab the value, live, from Query's input box as the player types it using JS or jQuery. But that's not available to the API or the sheet sandbox. The next option is to grab the value after it is sent to chat - this is the realm of API and cannot be done purely by the old vanilla sheetworkers as far as I'm aware. A sheet can't rely on API functions if you wish to submit it to Roll20, though of course you're free to use API-only functions if it's a custom sheet for your own use. const rxGrab: I don't really get what the need for this regex is?  It's looking for 0[something] is that due to the structure of something Roll20 has done? You absolutely don't need the regex there - you could isolate the string you need any way you wish. However, it must be an inline roll label to avoid breaking the roll with invalid input, and it must be an inline roll to be parsed by startRoll(). As above, this is the only way I'm aware of, without API, of pinching player input during a roll action. So you can use split(), slice() or whatever String methods you wish to grab the innards from the roll data. I default to regex due to the huge amount of control it gives, and the fact that you must use regex for a bunch of things. In terms of raw efficiency for simple matching, I believe it's inferior to your basic String methods like indexOf() or a simple === match, but that's more the realm of the GiGses and the Scottses and the Aaronses. edit - whoops, should probably mention that the whole inline label thing is only for values that aren't permitted in an inline roll, like text. If the only values you're collecting from the player query is an integer, you can just wrap it directly as an inline roll, [[?{input}]], and then lift it from the object returned by startRoll() in the 'total' property instead of needing to isolate it from the 'expression' property. 2. ?{ } is the basic setup for a query, with | being used to set a value and other factors such as creating a list.  I don't need all that extra just to take the value from ?{PlayerInput} and store it in attr_PsyUsed or some such. But how are you going to take that value from ?{playerInput} and get it into a sheetworker while it's still running . I'm not aware of another non-API method, but I could be missing something!
1634792267
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Hey Dustin, Welcome to roll parsing! Oosh has given you good run down of why that helper function is written the way it is. I think it'd help us to help you if you could explain what your goal is. What's the system rule that you are trying to handle or the table issue you're trying to solve?
1635130166

Edited 1635130292
It ended up being a busy weekend for me so I haven't got back to this issue just yet myself.  Oosh, thanks for that breakdown  Oosh said: Hi Dustin, edit - whoops, should probably mention that the whole inline label thing is only for values that aren't permitted in an inline roll, like text. If the only values you're collecting from the player query is an integer, you can just wrap it directly as an inline roll, [[?{input}]], and then lift it from the object returned by startRoll() in the 'total' property instead of needing to isolate it from the 'expression' property. So if it's just an integer then it can be stored simply?  I'm not looking for the completed roll value though, just the value of that query. Sorry Scott, I guess I'm not understanding your question.  System Rule? value="!roll40k @{character_name}, ?{Focus Power Characteristic | Willpower,@{Willpower} | Perception,@{Perception} | Psyniscience,@{PsyniscienceCharacteristic}}, [[(@{PsyRating} - ?{Psy Use?|1}) *10 + ?{Modifier|0}]]" All I know is that in this roll(the value section correct?) I need to pull out the value entered by the player for the query "?{Psy Use?|1}" so  that I can use it for 2 purposes: 1) in the flavor text I've been asked to add for Psy powers in the same roll 2) for a followup roll that would add the value to the damage for specific abilities As Oosh stated, outside of Roll20 it would be as simple as grabbing the live value, but that's not the case here.  I know there are reasons for the way things are done in Roll20, one being security, but that's where my confusion comes in.  I'm not aware of the workarounds necessary.  I guess it might also be necessary to mention this value comes from a repeating section, though that also wouldn't matter outside of Roll20 due to being capable of grabbing the live value. I need to grab the value, check to make sure it's an integer, and then apply it where needed.
1635155066

Edited 1635303808
Oosh
Sheet Author
API Scripter
Wait.... you're modifying the API script, and not the character sheet? Using the character sheet approach: There's a few ways to approach what you want to do. You can avoid custom roll parsing altogether if you want to, by re-using the modifier by just repeating ?{Modifier} in the same macro, then nesting it inside an [Ability](~button) with the rest of the follow up roll. I wouldn't recommend this though, as it's very clunky and makes for some horrid code to maintain, complete with escaped HTML. The next option is to single startRoll() with the roll you have above, and wrap the [[ ?{Modifier} ]] in inline brackets. This will make the value available in the startRoll() object for you to grab and do what you want with. You'll still need to reference ?{Modifier} (it must be an exact string match of the Query label) further along in your output if you wish to re-use the number in the same roll. This is the simplest way to reuse the value in the same roll - also note that if the modifier doesn't have too many possible values, you can do a form of data validation straight through the Query: ?{Modifier|1|2|3} will only allow those 3 values. I think the Query value should be nested inside the roll result object somewhere if it's in it's own [[ subroll ]], but I'm not 100% on that. The final option is to use the function you posted at the top, or something simplified. Personally I think this is the cleanest option: const getQuery = async ( userQuery ) => {     let rollBase = `! {{query1=[[ ${ userQuery } ]]}}` ,         queryRoll = await startRoll ( rollBase ),         queryResponse = queryRoll . results . query1 . result . total     finishRoll ( queryRoll . rollId );     return queryResponse ; }; You can then grab the value from the player and use it in your main roll without needing to bother with computed roll replacements - computed displays require writing a custom roll template in order to work, and more clunkiness. So the main roll might look like: const mainRoll = async () => {     let modifier = await getQuery ( `?{Modifier|0|1|2|3|4|5}` );     // Some kind of error checking on 'modifier' if required     let rollData = await startRoll ( `!roll40k @{character_name}, ?{Focus Power Characteristic |         Willpower,@{Willpower} | Perception,@{Perception} | Psyniscience,@{PsyniscienceCharacteristic}},         [[(@{PsyRating} - ?{Psy Use?|1}) *10 + ${ modifier } ]]` );     // Custom roll parsing would usually go here but I guess the API is already doing the heavy lifting?     finishRoll ( rollData . rollid ); } // And you just need an 'action' button somewhere on the sheet on ( 'clicked:mybutton' , () => mainRoll ); All that's left to do is throw a button on the sheet with type="action" and a matching name for the event handler. But if the sheet is already reliant on the API script, it's probably easier to just grab it there. To make it really simple, you can chuck an inline roll label on your modifier, for example ?{Modifier|0}[myMod] - making it extremely easy to isolate in a roll object which can get pretty crowded once you're dealing with big templates: on ( 'ready' , () => {     on ( 'chat:message' , ( msg ) => {         if ( msg . inlinerolls ) {             let rxLabel = / ( \d + ) \[ mymod \] / i ;             let reqRoll = msg . inlinerolls . find ( r => rxLabel . test ( r . expression ));             if (! reqRoll ) return ;             let modifier = reqRoll . expression . match ( rxLabel )[ 1 ];             log ( modifier );         }     }); }); This is a pretty basic standalone that'll just grab the first roll from a message that contains the [mymod] label, and grab whatever integers precede it. Not really sure on the contents of the rest of the script, or how you would integrate this in there (or if you even need to), but once you've got the modifier value you can set an attribute with it, or pass it to another function... whatever works.
Oosh said: Wait.... you're modifying the API script, and not the character sheet? Well, no I was trying to have it done on the character sheet, but yes there is an API script being used so I can see about plugging this in.  Thanks!
1635303594
Oosh
Sheet Author
API Scripter
Ah ok, I just noticed the roll button value was being sent to API and got a little confused. I've edited the post above with a couple of options.
Yea, I've been looking at other examples and from the best I can tell this is an old sheet using old methods and trying to put in minimal effort doesn't seem to be a realistic option lol. This is the Dark Heresy Advanced 2nd Edition sheet to be more specific.  The CSS styling is out of date, it doesn't use roll templates, and the API for it was calculating things incorrectly, which is what got me first started on it lol.  Finding the  ratten.html  sheet example has helped me to understand the startRoll() a bit better but I don't seem to be able to use the same method with the roll going into the value rather than in the rolltemplate. I had tried: <button name="roll_PsyHit" type="roll" value="***@{character_name} Attempts to Focus @{PsyName} at a PR of {{PsyUse}}!*** !roll40k @{character_name}, ?{Focus Power Characteristic | Willpower,@{Willpower} | Perception,@{Perception} | Psyniscience,@{PsyniscienceCharacteristic}},  [[(@{PsyRating} - {{PsyUse=?{Psy Use?|1}}}) *10 + ?{Modifier|0}]]"> <span data-i18n="hit-u">Hit</span> </button> But I guess that's trying to mix action buttons with roll buttons and the result is a button that functions but doesn't provide a result.  Then I tried changing the type to "action" to which I had a non-functioning button.  It has just been a process to try and wrap my head around how things work while dealing with a sheet that uses many out of date styles and trying to Frankenstein's Monster the entire thing.
1635305497

Edited 1635309873
Oosh said: But if the sheet is already reliant on the API script, it's probably easier to just grab it there. To make it really simple, you can chuck an inline roll label on your modifier, for example ?{Modifier|0}[myMod] - making it extremely easy to isolate in a roll object which can get pretty crowded once you're dealing with big templates: on ( 'ready' , () => {     on ( 'chat:message' , ( msg ) => {         if ( msg . inlinerolls ) {             let rxLabel = / ( \d + ) \[ mymod \] / i ;             let reqRoll = msg . inlinerolls . find ( r => rxLabel . test ( r . expression ));             if (! reqRoll ) return ;             let modifier = reqRoll . expression . match ( rxLabel )[ 1 ];             log ( modifier );         }     }); }); This is a pretty basic standalone that'll just grab the first roll from a message that contains the [mymod] label, and grab whatever integers precede it. Not really sure on the contents of the rest of the script, or how you would integrate this in there (or if you even need to), but once you've got the modifier value you can set an attribute with it, or pass it to another function... whatever works. So yes, this works as I need it to work and now I just need to figure out how to set an attribute to a value in API because it seems it's not the same as with sheetworkers(ala getAttrs & setAttrs).  You've been so much help, thanks!
1635312917
Oosh
Sheet Author
API Scripter
Ah yep, it's a different kettle of fish biscuits setting attributes with API. I'm a bit rusty on the old API front, but I think this should (maybe) do it: on ( 'ready' , () => {     const rxRoll = / ^ !roll40k\s/ i ;     const rxLabel = / ( \d + ) \[ mymod \] / i ;     on ( 'chat:message' , ( msg ) => {         if ( rxRoll . test ( msg . content ) && msg . inlinerolls ) {             let reqRoll = msg . inlinerolls . find ( r => rxLabel . test ( r . expression ));             if (! reqRoll ) return ;             let modifier = reqRoll . expression . match ( rxLabel )[ 1 ];             // Grab the @{character_name} reference from the roll expression             let charName = msg . content . match ( / ^ !roll40k\s + ([^ , ] + ) / i )[ 1 ];             let char = ( findObjs ({ type : 'character' , name : charName })||[])[ 0 ]             if (! char ) return ;             let charId = char . id ;             // Update the attribute if it exists, or create it if it doesn't             let attr = ( findObjs ({ type : 'attribute' , characterid : charId , name : 'modifier_store' })||[])[ 0 ];             if (! attr ) createObj ( 'attribute' , { characterid : charId , name : 'modifier_store' , current : modifier });             else attr . set ({ current : modifier });         }     }); }); The API doesn't know which character sheet initiates a roll unless the roll actually has that info in it. The message object does contain a msg.who property, but that only stores the name the sender has selected in their chat bar - it could be their player name, or their character name. So the most reliable way is to ensure the roll expression has the character name or id somewhere in it - in this case, it looks like all the rolls might start with !roll40k @{character_name}, So I used that as a base for the regex to pinch it. One obvious failing of this method is that it will fail if a character has a comma in their name, so the character " Hurling Frootmig, Out to Lunch " will break the script. You might also want some feedback in there instead of those returns. So grab the sender info from the message object (the "(GM)" appended to the gm's name always used to break sendChat(), not sure if this is still true but I've scrubbed it anyway): let sender = msg . who . replace ( / \( gm \) / i , '' ); Then you can throw a sendChat() after the "return" so the function will give some feedback before stopping execution: return sendChat ( 'modifierBot' , `/w " ${ sender } " Something went horribly wrong. Please reinstall universe.` ); Other than that... I think it should kind of work?
It works! I was slowly getting all the necessary knowledge but your help sped things up tremendously.  I only ran into an issue with the order in which the data was processed to which all I had to do was a bit of rearranging with the existing API code.  You were truly a timesaver on this, thanks so much! After looking around at roll parsing and roll templates I think I'm gonna work on updating this sheet so that it doesn't require API, but that's something I should be able to handle on my own.
1635376728
Oosh
Sheet Author
API Scripter
No problem, glad you got it working. There are plenty of helpful sheet authors around if you need a hand when you get to re-coding the sheet :)
1635574296

Edited 1635574542
Oosh said: No problem, glad you got it working. There are plenty of helpful sheet authors around if you need a hand when you get to re-coding the sheet :) Thought I'd share the code that I got working and see if there's anything you'd suggest not doing.  I am very out of practice with my coding so be gentle lol var Roll40k = Roll40k || (function(){ on('ready', () => { const rxRoll = /^!roll40k\s/i; const rxLabel = /(\d+)\[mymod\]/i; on('chat:message', (msg) => { if (rxRoll.test(msg.content) && msg.inlinerolls) { let sender = msg.who.replace(/\(gm\)/i, ''); log("Sender:"); log(sender); reqRoll = msg.inlinerolls.find(r => rxLabel.test(r.expression)); log("reqRoll:"); log(reqRoll); if (!reqRoll) return /*sendChat('modifierBot', `/w "${sender}" Something went horribly wrong in reqRoll. Please reinstall universe.`)*/; psyU = reqRoll.expression.match(rxLabel)[1]; log("psy rating:"); log(psyU); if (!psyU) return /*sendChat('modifierBot', `/w "${sender}" Something went horribly wrong in psyU. Please reinstall universe.`)*/; // Grab the @{character_name} reference from the roll expression let charName = msg.content.match(/^!roll40k\s+([^,]+)/i)[1]; log(charName); let char = (findObjs({type: 'character', name: charName})||[])[0] if (!char) return sendChat('modifierBot', `/w "${sender}" Something went horribly wrong. Please reinstall universe.`); let charId = char.id; // Update the attribute if it exists, or create it if it doesn't let attr = (findObjs({type: 'attribute', characterid: charId, name: 'psyuse'})||[])[0]; log("Attr:"); log(attr); if (!attr) createObj('attribute', {characterid: charId, name:'psyuse', current: psyU}); else attr.set({current: psyU}); } }); var rollResultForRoll40k = function (token, attribute, modifier) { var roll = randomInteger(100); var modTarget = parseInt(attribute) + parseInt(modifier); var output2 = token + ' has a modified target of <B>' + modTarget + '</B> and rolled a <B>' + roll + '</B>. '; var output3, degOfSuc; //Form output message based on result if (roll === 1) { degOfSuc = Math.floor((modTarget - roll) / 10) + 1; output3 = '<span style="color:green">' + token + ' rolled a 1 and critically succeeds with <B>' + degOfSuc + ' degree(s)</B>!</span>'; } else if (roll === 100) { degOfSuc = Math.floor((roll - modTarget) / 10) + 1; output3 = '<span style="color:red">' + token + ' rolled a 100 and critically fails with <B>' + degOfSuc + ' degree(s)</B>!</span>'; } else if (roll <= modTarget) { degOfSuc = Math.floor((modTarget - roll) / 10) + 1; output3 = '<span style="color:green">' + token + ' succeeds by <B>' + degOfSuc + ' degree(s)</B>.</span>'; } else { degOfSuc = Math.floor((roll - modTarget) / 10) + 1; output3 = '<span style="color:red">' + token + ' fails by <B>' + degOfSuc + ' degree(s)</B></span>.'; } //Return output: modified to have psy and non-psy output. if (!reqRoll) { log("weapon output"); return output2 + '<br><br>' + output3; }else { log("psy output"); var output1 = 'Psy Rating: ' + psyU; return output1 + '<br><br>' + output2 + '<br><br>' + output3; } }, processInlinerolls = function(msg) { if (_.has(msg, 'inlinerolls')) { return _.chain(msg.inlinerolls) .reduce(function(previous, current, index) { previous['$[[' + index + ']]'] = current.results.total || 0; return previous; },{}) .reduce(function(previous, current, index) { return previous.replace(index, current); }, msg.content) .value(); } else { return msg.content; } }; /** Interpret the chat commands. **/ on('chat:message', function (msg) { 'use strict'; var cmdName = '!roll40k '; if (msg.type === 'api' && msg.content.indexOf(cmdName) !== -1) { let content = processInlinerolls(msg); var paramArray = content.slice(cmdName.length).split(','); if (paramArray.length !== 3) { sendChat(msg.who, '/w ' + msg.who + ' You must specify three comma-separated ' + 'parameters for the !roll40k command.'); } else { log("Success!"); var result = rollResultForRoll40k(paramArray[0].trim(), paramArray[1].trim(), paramArray[2].trim()); sendChat(msg.who, result); } } }); }); }()) Thanks again! In regards to my logs, for some reason the API console didn't like this: log("some text" + var); Maybe I'm just that rusty on my JS but I thought that was how you were able to add a label to your logs, or maybe Roll20 uses their own log system.
1635636144

Edited 1635636488
Oosh
Sheet Author
API Scripter
If it's working, that's the important thing! I'm new to coding myself, and only a hobbyist, so my opinions are not necessarily worth that much, but the only things that leap out at me are a couple of examples of dated syntax. Using block-scoped 'let' and 'const' instead of the hoisted 'var' is almost always better practice, and template literals make for more readable code IMO. These are both more recent additions to the language (6ish years ago... up to you if 'recent' qualifies here), so there's still loads of code around that doesn't use them. So a string to send to the Roll20's log() would look like: const logPrefix = `ooshLog:` ; log ( ` ${ logPrefix } Error loading brain. Please reinstall human or contact garbage collection.` ); It's not a massive difference with a simple string like above, but when you get to writing transforms with roll20's repeating row attributes or something else with load of parts, it is much easier to read. Template literals also have the added bonus of casting values to Strings, giving you a bit of a shortcut for error correction on String methods: const inputString = undefined ; console . log ( inputString . indexOf ( 'defined' )); // Output: TypeError console . log ( ` ${ inputString } ` . indexOf ( 'defined' )); // Output: 2 Oh, and the other other added bonus that you can use both single ' and double " quotation marks freely inside the template literal's backticks ` ` without needing any escape characters - another big QoL improvement if you're constructing HTML strings full of both attributes and prose. Having said all that, I have no idea why this wouldn't work: log("some text" + var); Unless you have actually called your variable "var", but I'm assuming that's a placeholder :)