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 .
×
May your rolls be merry + bright! 🎄
Create a free account

Simulate a For Each loop to create a roll query of all the items in a repeating row

I am trying to create a quick macro for users to get a roll query that lists out their sheet's weapons to attack with.  Is this possible?
1533587030
GiGs
Pro
Sheet Author
API Scripter
You could do this with an API script. You cant do it with a standard macro.
G G said: You could do this with an API script. You cant do it with a standard macro. Okay, reading up on API scripts.  How do I get one to run from a Macro?  not seeing that yet.
1533588171
vÍnce
Pro
Sheet Author
What sheet are you using?  5e and the Pathfinder Community have companion scripts that may produce chat menus and/or ability macros.
Vince said: What sheet are you using?  5e and the Pathfinder Community have companion scripts that may produce chat menus and/or ability macros. Star Wars Saga Edition.
1533589578
GiGs
Pro
Sheet Author
API Scripter
It's likely you'd need to write your own API script to do this, unless there's a script someone's already written that's similar enough to tweak for the purpose.
1533590818

Edited 1533590904
vÍnce
Pro
Sheet Author
It would be cool if some scriptomancer was able to write a generic chat menu that would generate token ability macros for a character's repeating section(s).  "TokenChatMenu"  Select a linked token and provide the appropriate repeating section's name ie !tokenChatMenu --attacks Easy as that.  ;-P
1533594893
GiGs
Pro
Sheet Author
API Scripter
The tricky thing there is each character sheet uses different attributes, and attribute names, and rolls in different ways. You probably need to be able to construct the token ability roll from the appropriate attributes, and you have to know how the system works to do that.
1533602175
vÍnce
Pro
Sheet Author
G G said: The tricky thing there is each character sheet uses different attributes, and attribute names, and rolls in different ways. You probably need to be able to construct the token ability roll from the appropriate attributes, and you have to know how the system works to do that. I see.  Could the API be used to evaluate a repeating row to determine which attribute is a button or check for type="roll" and then use that attribute's name to construct the api command button?  It doesn't need to be said but..., I'm not a programmer. ;-P
1533602959
GiGs
Pro
Sheet Author
API Scripter
I dont think so, I think the API only has access to attributes.
1533648829
The Aaron
Pro
API Scripter
Yeah, from the API point of view, I could come up with a collection of all the attributes for a given repeating row, but none of the buttons, and there's no way for a general purpose API script to know how to even organize those attributes.
The Aaron said: Yeah, from the API point of view, I could come up with a collection of all the attributes for a given repeating row, but none of the buttons, and there's no way for a general purpose API script to know how to even organize those attributes. I don't need the buttons.  I just need the attributes.  If I had a collection I could parse them as I see fit.
1533655081

Edited 1533655289
The Aaron
Pro
API Scripter
So, here's a function that given character id and the name of the repeating group, will give you all the attributes in that group: const getRepeatingForCharacter = (charID, repeatingName) => { const matcher = new RegExp(`^repeating_${repeatingName}_`,'i'); return findObjs({ type: 'attribute', characterid: charID }).filter((a)=>matcher.test(a.get('name'))); }; A more complex version can be made which groups them by the row they're in. Actually, here's that more complex version: const getRepeatingRowsForCharacter = (charID, repeatingName) => { const matcher = new RegExp(`^repeating_${repeatingName}_(-[^_]+)_`,'i'); return findObjs({ type: 'attribute', characterid: charID }).filter((a)=>matcher.test(a.get('name'))) .reduce((m,a)=>{ let match = matcher.match(a.get('name')); m[match[1]]=m[match[1]]||[]; m[match[1]].push(a); return m; },{}); };
The Aaron said: So, here's a function that given character id and the name of the repeating group, will give you all the attributes in that group: const getRepeatingForCharacter = (charID, repeatingName) => { const matcher = new RegExp(`^repeating_${repeatingName}_`,'i'); return findObjs({ type: 'attribute', characterid: charID }).filter((a)=>matcher.test(a.get('name'))); }; A more complex version can be made which groups them by the row they're in. Actually, here's that more complex version: const getRepeatingRowsForCharacter = (charID, repeatingName) => { const matcher = new RegExp(`^repeating_${repeatingName}_(-[^_]+)_`,'i'); return findObjs({ type: 'attribute', characterid: charID }).filter((a)=>matcher.test(a.get('name'))) .reduce((m,a)=>{ let match = matcher.match(a.get('name')); m[match[1]]=m[match[1]]||[]; m[match[1]].push(a); return m; },{}); }; Now all I have to do it figure out how to make an API script to use it. :P
1533655700
The Aaron
Pro
API Scripter
=D  Yeah, that is more complicated.  So, can you spell out what you'd like it to spit out and to whom you want it to go?  I can maybe throw something together real quick like... =D
The Aaron said: =D  Yeah, that is more complicated.  So, can you spell out what you'd like it to spit out and to whom you want it to go?  I can maybe throw something together real quick like... =D Sure.  It's simple really.  I want my players to press an 'Attack' macro button that pops up a roll query that asks them which of their listed weapons they wish to attack with.  then it uses that for all of its rolls and such.
1533656654
The Aaron
Pro
API Scripter
Ah.  So, like most things, it's simple to say but less simple to do.  Here's how that would work in broad strokes: An API script would need to create an Ability on each Character which contains the generated Roll Query for all the weapons in a given repeating group for that character.  The Macro would then just execute the Ability on the selected character, or possibly you'd have the ability as a Token Action so that it shows up on the selected character. In slightly more detail, here's what the API script would be doing: On start up: • Find all Characters and all entries in the repeating group for weapons. • Find the Ability on each Character and rebuild the Roll Query On add/remove Entry • Find the Ability on the Character and rebuild the Roll Query Not tremendously hard to do, but slightly complicated.  If you can get me an example of how the Roll Query should look, I can likely throw together a script that does the above in relatively short order.
Honestly, I wouldn't mind doing it myself but there is no great getting started type article on API Scripts.  Like how do I create one that is accessible from a Macro?
The Aaron said: Ah.  So, like most things, it's simple to say but less simple to do.  Here's how that would work in broad strokes: An API script would need to create an Ability on each Character which contains the generated Roll Query for all the weapons in a given repeating group for that character.  The Macro would then just execute the Ability on the selected character, or possibly you'd have the ability as a Token Action so that it shows up on the selected character. In slightly more detail, here's what the API script would be doing: On start up: • Find all Characters and all entries in the repeating group for weapons. • Find the Ability on each Character and rebuild the Roll Query On add/remove Entry • Find the Ability on the Character and rebuild the Roll Query Not tremendously hard to do, but slightly complicated.  If you can get me an example of how the Roll Query should look, I can likely throw together a script that does the above in relatively short order. Basically, the Roll Query should Ask something like 'Choose your weapon' then have the drop down for each of the weapons in their sheet.   @{repeating_attack_X_WeaponName} X being the rowId.  Then, based on the selected weapon, grab the other attributes for that row and process the attack rolls.
1533657883
The Aaron
Pro
API Scripter
Ah!&nbsp; Well THAT I can help with. =D You could try starting here: <a href="https://app.roll20.net/forum/post/6605115/namespaces-novice-seeks-help-exploring-the-revealing-module-pattern" rel="nofollow">https://app.roll20.net/forum/post/6605115/namespaces-novice-seeks-help-exploring-the-revealing-module-pattern</a> <a href="https://app.roll20.net/forum/post/6584105/creating-an-object-that-holds-specific-character-dot-id-and-character-name/?pagenum=1" rel="nofollow">https://app.roll20.net/forum/post/6584105/creating-an-object-that-holds-specific-character-dot-id-and-character-name/?pagenum=1</a> <a href="https://app.roll20.net/forum/post/6237754/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/6237754/slug%7D</a> And chances are, there are other posts to cover any other problems you run into. To answer the "how can I call it from a macro" question more directly, a macro (or ability, or the chat area) would most easily activate an API script by using an API Command.&nbsp; An API Command is any message where the first character of the message is a ! such as: !do-my-thing with my args The API is Event Driven, which means that all scripts are only ever executed once, when the API Sandbox starts up (when someone joins the game and it's not running, or when you save a script and it restarts).&nbsp; Their responsibility during that execution is to register functions which will respond to various events.&nbsp; In the case of an API command, the event you are interested in is chat:message. &nbsp; A simple chat:message event handler might look like this: on('chat:message',(msg) =&gt; { if( 'api' === msg.type &amp;&amp; /^!do-my-thing\b/i.test(msg.content)){ sendChat('demo script', 'You ran &lt;code&gt;!do-my-thing&lt;/code&gt;!'); } }); (Note: this is using Javascript ES6+ syntax, so don't worry if you know javascript and this looks like gibberish.&nbsp; Just read the links above and you'll feel loads better!) There are events for most of what the humans do, so you can react to them.&nbsp; After setting up your chat:message &nbsp;event handler, you'd just have the Macro call it and Bob's yer uncle. Post back with whatever questions you have. =D&nbsp; Also, we have a (mostly up-to-date) wiki:&nbsp; <a href="https://wiki.roll20.net/API:Introduction" rel="nofollow">https://wiki.roll20.net/API:Introduction</a>
Ok so I have that, or at least the beginnings.&nbsp; How do I build the Roll Query?&nbsp; Is it just a string that gets built like I were typing it into the chat window?
1533661792

Edited 1533661836
The Aaron
Pro
API Scripter
Probably.&nbsp; Lets assume the command for an attack looks something like: Attack with Sword, [[1d20+@{Bob the Slayer|repeating_weapons_SOMEID_weapon_to_hit}]] does [[@{Bob the Slayer|repeating_weapons_SOMEID_weapon_damage}]] You'd need to first convert that to whatever you'd do with a Roll Query. ?{Which weapon|Sword,Attack with Sword, [[1d20+@&amp;#123;Bob the Slayer&amp;#124;repeating_weapons_SOMEID_weapon_to_hit&amp;#125;]] does [[@&amp;#123;Bob the Slayer&amp;#124;repeating_weapons_SOMEID_weapon_damage&amp;#125;]]} Then you'd need to generate that in the code dynamically and write it into an Ability object for that character.&nbsp; Far easier (and probably what you really want) is to trigger the roll on the character sheet.&nbsp; You can hit the roll button, then press up and get the command for it, and use that as your template.&nbsp; Then your command might look more like: ?{Which weapon|Sword,%&amp;#123;Bob the Slayer&amp;#124;repeating_weapons_SOMEID_weapon_roll&amp;#125;} At which point, you're just building that dynamically with the list of weapons in that repeating group, something vaguely like: let weapons = getRepeatingRowsForCharacter(character.id,'weapons'); let query = `?{Which weapon|${Object.keys(weapons).map(id =&gt; { let nameAttr = weapons[id].find(a =&gt; /_name/i.test(a.get('name'))); return `${nameAttr.get('current')},%&amp;#123;${character.get('name')}&amp;#124;repeating_weapons_${id}_weapon_roll&amp;#125;`; }).join('|')}}`;
The Aaron said: So, here's a function that given character id and the name of the repeating group, will give you all the attributes in that group: const getRepeatingForCharacter = (charID, repeatingName) =&gt; { const matcher = new RegExp(`^repeating_${repeatingName}_`,'i'); return findObjs({ type: 'attribute', characterid: charID }).filter((a)=&gt;matcher.test(a.get('name'))); }; A more complex version can be made which groups them by the row they're in. Actually, here's that more complex version: const getRepeatingRowsForCharacter = (charID, repeatingName) =&gt; { const matcher = new RegExp(`^repeating_${repeatingName}_(-[^_]+)_`,'i'); return findObjs({ type: 'attribute', characterid: charID }).filter((a)=&gt;matcher.test(a.get('name'))) .reduce((m,a)=&gt;{ let match = matcher.match(a.get('name')); m[match[1]]=m[match[1]]||[]; m[match[1]].push(a); return m; },{}); }; Do you have some RegEx to grab something&nbsp;that matches this:&nbsp;repeating_attack_-LJKLZBfsruYDNKvTUWS_WeaponName? I tried&nbsp; `^repeating_${repeatingName}_[\w/-_]+_WeaponName` But it doesn't like it
1533672353

Edited 1533672378
The Aaron
Pro
API Scripter
I'd use: `^repeating_${repeatingName}_-[^_]+_WeaponName`
The Aaron said: I'd use: `^repeating_${repeatingName}_-[^_]+_WeaponName` thanks.
I have an issue with the code you gave me.&nbsp; (shown below)&nbsp; getting the error: TypeError: weapons[id].find is not a function on('chat:message', (msg) =&gt; { &nbsp; &nbsp; //log(msg); &nbsp; &nbsp; if('api' === msg.type){ &nbsp; &nbsp; &nbsp; &nbsp; let args = msg.content.split(/\s+/); &nbsp; &nbsp; &nbsp; &nbsp; let character = findObjs({type: 'character', name: args[1]})[0]; &nbsp; &nbsp; &nbsp; &nbsp; if(character){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let weapons = getRepeatingForCharacter(character.id,'attack'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let query = `?{Which weapon|${Object.keys(weapons).map(id =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let nameAttr = weapons[id].find(a =&gt; /_name/i.test(a.get('name'))); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return `${nameAttr.get('current')},%{${character.name}|repeating_weapons_${id}_WeaponCheck}`; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }).join('|')}}`; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('SYSTEM', '/w ' + character.name + ' ' + query); &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('SYSTEM', '/w ' + msg.who + ' Invalid player'); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; } }); const getRepeatingForCharacter = (charID, repeatingName) =&gt; { const matcher = new RegExp(`^repeating_${repeatingName}_-[^_]+_WeaponName`,'i'); return findObjs({ type: 'attribute', characterid: charID }).filter((a)=&gt;matcher.test(a.get('name'))); };
1533691027

Edited 1533691043
The Aaron
Pro
API Scripter
Hmm.. you can change it to: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let nameAttr = (weapons[id]||[]).find(a =&gt; /_name/i.test(a.get('name'))); and avoid the error, but what that's telling me that the id isn't in the list, which is weird.&nbsp; You might need to do a bit of debugging, or I'll try to at some point soon... getting kids to bed now... might be tomorrow...
So what it looks like is that find() is an array method.&nbsp; But weapons[id] is not an array.&nbsp; It is a string that looks like JSON, but when I try to do a JSON.parse() the error indicates that it is already an object.&nbsp; Yet it doesn't act like a JS object at all.&nbsp; Very strange.
1533740917
The Aaron
Pro
API Scripter
It should be an array.&nbsp;&nbsp; getRepeatingRowsForCharacter() should return an object with this structure: { 'ROW ID' : [ ATTRIBUTE_OBJECT, ATTRIBUTE_OBJECT, ... ], 'ROW ID' : [ ATTRIBUTE_OBJECT, ATTRIBUTE_OBJECT, ... ], ... } Object.keys() should return and array of all the ROW ID values, which .map() then gets as id and uses to reference into weapons[] to get the array of ATTRIBUTE_OBJECTS, which it then searches.&nbsp; Somehow, it seems like the ids that it's getting aren't in the variable.&nbsp; Is it possible you have two variables with similar spelling?&nbsp; Maybe paste your code and I can look at it, or PM it if you don't want it in the thread.
This is what I get back from that function: [{"name":"repeating_attack_-LJKLZBfsruYDNKvTUWS_WeaponName","current":"Blaster Pistol","max":"energy","_id":"-LJKL_PSmChhoo44rhLG","_type":"attribute","_characterid":"-LJKLSgHeamHIxGccJwn"},{"name":"repeating_attack_-LJKLjBzkZKUPaS0iS2t_WeaponName","current":"Vibroblade","max":"slashing","_id":"-LJKLkQ4NFHNktfhl2Zv","_type":"attribute","_characterid":"-LJKLSgHeamHIxGccJwn"}] which looks like an array of JSON objects to me.
1533741539

Edited 1533741894
Here is the function: &nbsp;&nbsp;&nbsp;&nbsp;const getRepeatingForCharacter = (charID, repeatingName) =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const matcher = new RegExp(`^repeating_${repeatingName}_-[^_]+_WeaponName`,'i'); &nbsp; &nbsp; return findObjs({ &nbsp; &nbsp; type: 'attribute', &nbsp; &nbsp; characterid: charID &nbsp; &nbsp; }).filter((a)=&gt;matcher.test(a.get('name'))); &nbsp; &nbsp; };
1533744066
The Aaron
Pro
API Scripter
OH!&nbsp; I was calling the second function in my sample code: const getRepeating Rows ForCharacter = (charID, repeatingName) =&gt; { const matcher = new RegExp(`^repeating_${repeatingName}_(-[^_]+)_`,'i'); return findObjs({ type: 'attribute', characterid: charID }).filter((a)=&gt;matcher.test(a.get('name'))) .reduce((m,a)=&gt;{ let match = matcher.match(a.get('name')); m[match[1]]=m[match[1]]||[]; m[match[1]].push(a); return m; },{}); }; You're right, that first one just returns an array of attributes: [ ATTRIBUTE_OBJECT, ATTRIBUTE_OBJECT, ... ] BTW, the log() function does a JSON.stringify() on the contents, which is why that output looks like JSON data.
hmm.&nbsp; that's even stranger then.&nbsp; This code:&nbsp; Object.keys(weapons).map(id =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let weapon = weapons[id]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let name = weapon.current; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; log(name); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return name; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); the log(name) returns undefined.&nbsp; but this is what weapons looks like: [{"name":"repeating_attack_-LJKLZBfsruYDNKvTUWS_WeaponName","current":"Blaster Pistol","max":"energy","_id":"-LJKL_PSmChhoo44rhLG","_type":"attribute","_characterid":"-LJKLSgHeamHIxGccJwn"},{"name":"repeating_attack_-LJKLjBzkZKUPaS0iS2t_WeaponName","current":"Vibroblade","max":"slashing","_id":"-LJKLkQ4NFHNktfhl2Zv","_type":"attribute","_characterid":"-LJKLSgHeamHIxGccJwn"}]
1533744982
The Aaron
Pro
API Scripter
You have to access properties on Roll20 Objects via the .get() method: Object.keys(weapons).map(id =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let weapon = weapons[id]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let name = weapon .get('current') ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; log(name); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return name; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); This has to do with the way that those objects are synchronized via Firebase to the game instances of all the players.&nbsp; Using a method for .get() and .set() allows them to lazy load the contents, and trigger synch events on sets.