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

[Script] Roll Handout Tables

1604327021

Edited 1608836724
Jordan C.
Pro
API Scripter
Pending:  12/24: The one-click version of the script seems to have issues that I am unable to identified; however, the "import" version of the script from the library functions as expected. Update 2.0 - Now works with DMG tables! (Huge thanks to Aaron for helping me test the new tables) The script has been completely rewritten and functions with nearly all tables I could think to test excluding a few 'problem' tables that I will list below. Table naming has been improved though a select few can still return lengthy names.  Should be ready for One-Click on 12/22 Updates: Table names replace spaces with '_' and removes commas, colons, and parens. Can handle multi-column tables such as Treasure Hoard tables. Can create multiple tables for split dice tables (e.g. Passages table that uses d12 or d20). Works with multiple dice roll tables, but does not create accurate item weights for them (e.g. 3d8) Amended:  Now handles specified instances of multiple dice rolls (3d4, 2d6, 3d6, and 3d8) Added a very  crude toggle to stop writing handout links when you enter !handoutlinks off, and can turn it back on using !handoutlinks on. This setting does not  save once the game is reset. I haven't figured out how to do that part yet. Will now create items/tables when it sees something like "10 or lower" or "3d8+CHA", but still can't account for previous influencing rolls/stats Tables with known issues: Childhood memories: (3d8 + CHA), Running a business (d100+days), Carousing  (d100+level): Creates tables but can't account for adjusted weights from previous modifiers. Selling a Magic Item (d100 + mod.) Siblings and Home table from Tasha's Origins (requires subtracting numbers which item weight cannot account for Magic Items Table I, Villains Methods and Schemes , has 'subtables' within the table(I've marked this as too niche to fix for the time being but subject to change) Designing NPC tables that have nested tables, (e.g. Ideals ) Until the update is accepted, you can find the full script here:  Github Repo 1.5:  Added better table naming handling, specifically for the tables in Tasha's Cauldron of Everything. Now works with every table I could think to test from the compendium. Names can get lengthy, but it's difficult to catch all conditions otherwise. Not yet updated with one-click but available via github link. Set default rolls to whispers. 1.4: Cleaned up the formatting and used better variable naming, replaced crude handler with a cleaner one for endless loop that occurs (setting notes triggering change handout event). Thanks Aaron for the code framework/suggestion for that 1.3:   Added better variable handling for links and added " " in front of items that start with digits that aren't rolls. Fixed a bad variable name. Hello all, Now available via one-click install I finished the first version of a script that will search all handouts in your game, check if they contain a rollable table, create that table if they do, then write a link above that table in the handout notes that will roll from that table into chat. Many thanks to The  Aaron and Keith for the help/idea for this script. Notes: This is a great way to organize and keep track of mass amouts of roll tables since you can use folders with handouts! Parses all handouts using "!rollhandout" as an api chat command This script was written to work in conjunction with recursive tables but there are standard 'roll' links included along side the 'roll recursive' links. One caveat I haven't addressed is that it will always surround any dice roll with brackets (e.g. 1d6+2 turns into [[1d6+2]]) The script as is will run at the start of a game being launched and scan every handout immediately. If you wish to prevent this, there are lines that can be uncommented. (1, 9, 14, 218, and 222 I believe; they all have comments labelling this feature). The script will parse any handouts that are added. For example, dragging "Arctic Encounters" from the 5e compendium to the game will create a handout, proc the script to make tables for each tier and add a link to that handout automatically. I am unaware of any conflicts at the moment, but if you notice any variable names that might be an obvious conflict I am happy to adjust this. I am very, very new to this so if anything at all stands out as a potential problem or just bad practice I won't be at all offended by it being pointed out. I know some formatting for the chat commands could be improved, but I plan to handle that after some feedback for what people may be looking for, if anything. I hope some may find this script useful and if you encounter any errors please let me know! My tests were run mostly with the standard tables from 5e compendium so it is possible different tables may cause an issue. If you have any suggestions or recommendations for improving this I am more than happy to learn how to implement them if possible. Thanks!
1604333475
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Hi Jordan, I have tried importing a couple of handouts with tables but am unable to get the script to create buttons. I am using the Arctic Encounters test you mention. Also, the sandbox crashes unless I comment out the first and last lines. Any idea what I am doing wrong?
1604334484
Jordan C.
Pro
API Scripter
keithcurtis said: Hi Jordan, I have tried importing a couple of handouts with tables but am unable to get the script to create buttons. I am using the Arctic Encounters test you mention. Also, the sandbox crashes unless I comment out the first and last lines. Any idea what I am doing wrong? Keith,  Do you know what the API sandbox throws as the error? It can technically work without the 'on ready' it will just re-analyze everything at each start of the game. As for running the script to accommodate handouts that are added in after the initial pass, the lines that need to be reinstated should look like the ones below (it's possible I got the line numbers wrong): Line 1: on('ready', function() { //comment out for first run Line 9: on('change:handout', function(obj) { //comment out for first run Line 14: i++; //comment out for first run Line 218: )}; //end change:handout - comment out for first run Line 222: )}; //end on ready - comment out for first run I made a fresh game and ran the script by itself and it functioned as expected so there may be a conflict with another script? Another possible troubleshoot is to delete the handout from the journal and drag it over again. 
1604339385
GiGs
Pro
Sheet Author
API Scripter
I have only skimmed the script, and am too tired to read it properly, but I'm puzzled by the commented out sections. The first and last lines, commenting out the on(ready) statement are most baffling to me. What purpose and effect is this meant to have? All on('ready'_ does is delay the start of the script until the campaign is fully loaded, and as I understand it, there is no benefit to having those lines commented out, and uncommented them wont change the function of the script at all - but will make it more robust. Another question: if you were to uncomment them all, you'd have an on('change:handout') event nested inside an on('add:handout') event. That doesnt seem right to me.  Final question: you have this loop: for (r; r < row.length; r++) { but you have earlier (line 55 i think) defined let r = 0 The for loop will be using that r value, and will not start at zero. Is that intended? I'm guessing it doesnt set it at let r= 0, it is intended, it just struck me as odd and worth asking about.   Regarding your uncommenting approach: it would be better to handle things like this without uncommenting. Instead set a variable near the start of the script, which can maybe have a value of 0 or 1, and when coming to those commented lines, check that variables value, and only do the operations if the value is 1. That makes it much easier for people to switch functions and stuff off and on - you just tell them to change the value at the start of the script.
1604341066

Edited 1604341370
Jordan C.
Pro
API Scripter
The first and last lines, commenting out the on(ready) statement are most baffling to me. What purpose and effect is this meant to have? All on('ready'_ does is delay the start of the script until the campaign is fully loaded, and as I understand it, there is no benefit to having those lines commented out, and uncommented them wont change the function of the script at all - but will make it more robust. The intent I had was to analyze all handouts that already exist in a game and are being adding as the game loads. For instance if someone has a few dozen handouts with rollable tables already made they would have to delete them then re-add them for the script to run and parse them. Instead, you can comment out those lines on its first run and have all handouts be parsed and reinstate them afterwards Another question: if you were to uncomment them all, you'd have an on('change:handout') event nested inside an on('add:handout') event. That doesnt seem right to me.  The intent here is to not run the script every time a handout is personally edited. If it only used the change:handout  event it would run anytime someone added something trivial which I figured could just be bypassed by using add:handout.  If I used add:handout  by itself, it would read the notes section as null for any handout that is dragged over since it seems to create the handout, then change it as a separate event. If I am missing something here, let me know. Final question: you have this loop: for (r; r < row.length; r++) { but you have earlier (line 55 i think) defined let r = 0 The for loop will be using that r value, and will not start at zero. Is that intended? I'm guessing it doesnt set it at let r= 0, it is intended, it just struck me as odd and worth asking about. I may not have gone about it in the most elegant way by I was intending to reset and define r as 0 and only have it start at 1 if a certain condition is met. It's entirely possible that it is completely unnecessary and already has a value of 0 without that line. The intent here was to address the lack of uniformity in the tables that get created from compendium items. Below are two examples from the Sorcerer handout that is created by dragging it into the game; in the first example the relevant table information starts at a different row than the second example. I have found at least 3 separate examples of tables: Has a <thead> tag but is blank, has <thead> that isn't blank, doesn't have <thead> section. The simplest method I thought of was just setting the starting row at the relevant number in regards to the loop.  Example 1: Example 2: Regarding your uncommenting approach: it would be better to handle things like this without uncommenting. Instead set a variable near the start of the script, which can maybe have a value of 0 or 1, and when coming to those commented lines, check that variables value, and only do the operations if the value is 1. That makes it much easier for people to switch functions and stuff off and on - you just tell them to change the value at the start of the script. That's a great solution, thank you! Definitely my laziness showing there but that's an easy enough way to handle those. Thanks for the feedback!
1604341603

Edited 1604341644
The Aaron
Roll20 Production Team
API Scripter
I haven't had the chance to read this script yet, sorry about that. =( I will say that if you want to get this included in the Roll20 Repo, you will want all configuration to not require editing the script.  I suggest the state object for that.  Regarding the events, it would likely be better to wait until on('ready',...) has fired, then just do a findObjs({type:'handout'}) and handle each of them there (likely with a deferred queue).  The problem with registering event handlers inside of events is that you end up with as many handler calls as the number of times your event occurs.  Consider if you have twenty handouts in your game, registering an on('change:handout',...) event in your on('add:handout',...) handler means the next time you change a handout, you're handler for editing it will be called 20 times to do the same work.
1604342944
Jordan C.
Pro
API Scripter
The Aaron said: I haven't had the chance to read this script yet, sorry about that. =( I will say that if you want to get this included in the Roll20 Repo, you will want all configuration to not require editing the script.  I suggest the state object for that.  Regarding the events, it would likely be better to wait until on('ready',...) has fired, then just do a findObjs({type:'handout'}) and handle each of them there (likely with a deferred queue).  The problem with registering event handlers inside of events is that you end up with as many handler calls as the number of times your event occurs.  Consider if you have twenty handouts in your game, registering an on('change:handout',...) event in your on('add:handout',...) handler means the next time you change a handout, you're handler for editing it will be called 20 times to do the same work. No worries, and thanks for the feedback! I'll have to admit that I don't fully grasp what is going on with regards to why the event handler goes 20 times for having 20 handouts. In its normal state I have it set to start with on('ready. For my purposes of editing existing tables just the one time I found it easier to just bypass a few lines briefly then return it to the state it would function in for the rest of its intended use. I think I was understanding the add/change events as "if a handout is added, and it is followed by that handout being changed, then proceed with the script." In my testing that has been my experience, but is that not what is happening? Would it be better to just have the on('change: and have it activate the few times someone edits a handout without a table? As for the deferred queue and findObjs, I haven't learned quite enough yet to understand that fully; I would be concerned that it tries to parse hundreds of handouts just to find the one that has a new table, which likely shows that I would be implementing it incorrectly so any help there would be wonderful. For simplicity sake I can easily remove the section about commenting out lines and the script functions as intended, it just won't retroactively add tables to handouts. From there I can update a newer version once I have worked out how to implement the states.
1604343074
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
It was error on my end. I didn't fully understand which lines to comment out for which purpose. I will try again this evening.
1604344458
The Aaron
Roll20 Production Team
API Scripter
Event handlers are not specific to a single instance of a Roll20 type.  If you register an event handler for 'change:handout', it will get called for every handout that is changed, regardless of where you created the handler.  The event system behind the on() function has a list of functions to call when a matching event occurs.  Each time you call it, it will add the supplied function to the list for that event.  It does not remove duplicates, so if you call it with the same function 100 times, add 100 copies of that function to its list, and it will call that same function 100 times when that 1 event occurs.  If you are adding event handlers inside the handler for another event, then they will get re-added each time that event occurs, leading to duplicates. Would it be better to just have the on('change: and have it activate the few times someone edits a handout without a table? The way you have it now will not prevent it from being called for every handout that is edited, table or no.  You will need to inspect the contents of the handout to determine if you want to parse it regardless.  You could require a naming convention in the handout title to make it easy to pare down the list, but you'll still need to parse the contents and determine if they are valid. As for the deferred queue and findObjs, I haven't learned quite enough yet to understand that fully; I would be concerned that it tries to parse hundreds of handouts just to find the one that has a new table, which likely shows that I would be implementing it incorrectly so any help there would be wonderful. As it stands, you're already parsing all those handouts at start up, it's just happening as many single calls to your event handler as the API spins up. (which you are ignoring with commented/uncommented commands, but should really manage differently)  A deferred queue looks something like this: let handouts = findObjs({type:'handout'}); const parseOneHandout = () => { if(handouts.length){ let handout = handouts.shift(); // do something with handout; // defer next handout setTimeout(parseOneHandout,0); } }; // start the deferred processing parseOneHandout();   Adding a chat command to kick of the parsing of all handouts, or possibly a filtered subset would be a way to dynamically kick off parsing of a subset of handouts, or parsing at a time of choice.
1604347504

Edited 1604347623
Jordan C.
Pro
API Scripter
Okay, it will take me some time to digest everything but thanks for your patience and describing it thoroughly! As for the commenting/uncommenting part I totally concede that it needs to be removed and I am now looking at the script without that as part of the equation. I will come at that after overcoming the event problems it has. If you register an event handler for 'change:handout', it will get called for every handout that is changed, regardless of where you created the handler. I think my confusion is stemming from not seeing the effects of this in my logs during testing. For instance, I had logs directly after each event saying things like "handout xyz created" and "handout xyz changed"; with what your describing, would the script not create logs for every handout that exists? And wouldn't it send that log when a handout is edited even without the 'add:handout' event? Or let's say there were a bunch of logs within the function after that part like "log("no table found"), wouldn't it log 100 times if the function was stored 100 times? Quick edit: I think I am also confused since the logs/script seemed to work exactly as expected and took next to zero time to complete its tasks, which may not be a relevant factor to the topic but is contributing to my brain block. I apologize for not fully understanding and I certainly don't want any of this to come across as stubborn, so please forgive me if it is.  As it stands, you're already parsing all those handouts at start up, it's just happening as many single calls to your event handler as the API spins up. (which you are ignoring with commented/uncommented commands, but should really manage differently)  A deferred queue looks something like this: Thank you for the example, that definitely makes more sense now!
1604349029

Edited 1604349042
The Aaron
Roll20 Production Team
API Scripter
No worries.  I'll try to work up a little example script that illustrates it more eloquently. =D
1604351347
Jordan C.
Pro
API Scripter
Thank you! In the meantime I have added your deferred queue suggestion and now it no longer has any elements of nested events or anything like that. It simply activates on "!rollhandout" chat command. Now it doesn't handle when a handout is created by dragging, but would it cause any issues to simply name the function that is called for the on('chat:message') event and also call it on an 'change:handout' event? Or can I handle the single object from that change handout event without having to deal with the other handouts? Or am I just running in a mental circle and would have the same problem I had before 
1604354127
The Aaron
Roll20 Production Team
API Scripter
I'd do something like: on('ready',()=>{ const processSingleHandout = (handout) => { // do stuff here with handout }; const processAllHandouts = () => { let handouts = findObjs({type:'handout'}); const parseOneHandout = () => { if(handouts.length){ let handout = handouts.shift(); processSingleHandout (handout); // defer next handout setTimeout(parseOneHandout,0); } }; // start the deferred processing parseOneHandout(); }; const onChangeHandout = (obj, prev) => { processSingleHandout (obj); } on('chat:message',(msg)=>{ // some chat stuff that calls processAllHandouts() }); on('change:handout',onChangeHandout); });
1604364132

Edited 1604366118
Jordan C.
Pro
API Scripter
Thank you tons for the help!  I am gonna spend some time to figure out exactly what I just did but your framework worked well; I included a similar section to the last version that basically stops the process when it 'sets' notes since it technically would proc the change handout event again. (I did so using i as a random variable and a crude if function: (i > 0 ? i = 0 and return) essentially). Slightly confused by the (obj, prev) and how it works in place of 'handout' but I am sure I can read up on that. I also want to adjust the chat command that is sent by the standard roll but I am having difficulty with something like "1 adult red dragon" turning into just 1 in the chat. I am sure I will be able to find some information on that as well. Again, thank you. For the framework but also the patience! 
1604371286

Edited 1604371405
The Aaron
Roll20 Production Team
API Scripter
The names of the arguments for event functions are just convention. The first one, commonly obj, is the Roll20 object that caused the event in its current (final) state, so after the changes have been applied. The second, commonly prev, is a plain Javascript object with the properties the Roll20 object had before the event. For example, if someone moved a token from left 70  to left 140, the "change:graphic" event would fire and obj would be that graphic object, with obj.get("left") returning 140, and prev would be a Javascript object with a property left of 70 (and all the other properties the same as what you'd .get() them on obj.  API changes shouldn't trigger events, with rare exception. Rollable tables entries that start with a number are taken as die rolls. To prevent that, you need something in front of the number. A space might work, or   or the like. 
1604406063
Jordan C.
Pro
API Scripter
The Aaron said: The names of the arguments for event functions are just convention. The first one, commonly obj, is the Roll20 object that caused the event in its current (final) state, so after the changes have been applied. The second, commonly prev, is a plain Javascript object with the properties the Roll20 object had before the event. For example, if someone moved a token from left 70  to left 140, the "change:graphic" event would fire and obj would be that graphic object, with obj.get("left") returning 140, and prev would be a Javascript object with a property left of 70 (and all the other properties the same as what you'd .get() them on obj.  API changes shouldn't trigger events, with rare exception. Rollable tables entries that start with a number are taken as die rolls. To prevent that, you need something in front of the number. A space might work, or   or the like.  Noted! Those both are clear now. It would return 'maximum call stack size exceeded' without that bit being added so I just assumed that's what was happening. And okay, that should be easy enough to account for, thanks!
1604411649
The Aaron
Roll20 Production Team
API Scripter
Events would be asynchronous, so wouldn't be limited by the callstack size.  I'll have to look at the code, but probably you have accidentally created a runaway recursive function.
1604415882
Jordan C.
Pro
API Scripter
Ah, interesting I must really be missing something then - I removed the code that catches the error so that it would return the call stack exceeded again, then I only commented one line of code out and there was no more error. Here's the line I omitted: handout.set('notes', notesNew);
1604416688
The Aaron
Roll20 Production Team
API Scripter
Well!!  No, you're totally right, that must be causing a change event!!!   I'd consider that a bug. It's probably cleaner to have an array of changed handout IDs and manipulate it, but using a counter like you have it works. 
1604417274
Jordan C.
Pro
API Scripter
Oh as in: Push handout ID into a temporary cache Set notes And have the change event check if the ID exists in the temporary cache before continuing? If so I think I can add that 
1604417867
The Aaron
Roll20 Production Team
API Scripter
Yeah, something akin to; // just below on('ready',...) let activeChangeIds = []; // in the processSingleHandout() just before change activeChangeIds.push(handout.id); // in the onChange function if(activeChangeIds.includes(obj.id)){ activeChangeIds.remove(obj.id); } else { processSingleHandout(obj); }
1604497933
Jordan C.
Pro
API Scripter
Gotcha! The crude handler in there occupied those spaces so it was an easy swap, thanks! It is now at version 1.4 and is much cleaner than the other versions; if there is anything more that would be good to add/change for it to be an acceptable addition as a proper script I am happy to continue working on it.
1605636992

Edited 1605899142
Jordan C.
Pro
API Scripter
For anyone looking to import tables from Tasha's Cauldron of Everything, the table formats used upon handout creation are different (yet again somehow) and do not all work with the script. I am looking into it and will hopefully have it addressed quickly without breaking anything else. Edit:  Decided to just add a basic if statement that determines whether the section only has one table and if so it uses that as the table name preceded by the handout name. Will ignore if the table name was already formed from the section header.  Spoke too soon, the RollTable handouts have zero headers which breaks this script right now. Working on it. Maybe. Edit: Now works with Tasha's tables.
1607216272
The Aaron
Roll20 Production Team
API Scripter
Nice!  Have you tried it with the DMG tables?
1607216514
Jordan C.
Pro
API Scripter
I have been having trouble posting so hopefully this goes through: I haven't done any testing for the DMG as I haven't purchased it yet. I am not sure I plan to at the moment but I am more than happy to update the script if if issues arise!
1607546440
Jordan C.
Pro
API Scripter
Now works with DMG tables thanks to Aaron's support! This includes treasure hoard tables, dungeon mapping, wilderness mapping, etc. Even works on that pesky d12/d20 table too now ;)
1607547140
The Aaron
Roll20 Production Team
API Scripter
Nice!  I've got to say that script is a lot neater than I initially realized.  I love the embedded roll links in the handouts, very chic!
1607549422
Jordan C.
Pro
API Scripter
Well frankly the first version was pretty rough. But the idea for the script and that clever design suggestion were provided by none other than keithcurtis ! 
1607551709
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Hi Jordan, It's looking really slick! I installed in a brand new campaign and got no results until I realized I had not installed Recursive Tables. Is there a way to test for that and give a warning to new users?
1607552355
The Aaron
Roll20 Production Team
API Scripter
This: if('undefined' === typeof RecursiveTable){ setTimeout(()=>sendChat('Roll Handout Tables',`/w gm <div style="background:#ff9999;padding:.5em;border:3px solid darkred;border-radius:1em;line-height:1em;color:darkred;"><b>Roll Handout Tables</b> requires the script RecursiveTable, which can be installed from the 1-Click Script Library.</div>`),1000); }
1607552388
The Aaron
Roll20 Production Team
API Scripter
Of course, in 1-click, you can just declare RecursiveTable as a dependency and it will install it, but it's nice to check for peeps that copy/paste the source.
1607553227
Jordan C.
Pro
API Scripter
Keith, that warning should probably exist for everyone with api access in general lol. And wow that was fast! Just added it in, much appreciated.
1607559157
The Aaron
Roll20 Production Team
API Scripter
(I may have had 95% of that code sitting around for similar purposes... =D)
1607634842
Jordan C.
Pro
API Scripter
Now enters proper weights for 3d4, 2d6, 3d6, and 3d8 tables!
1608827215
Jordan C.
Pro
API Scripter
Update: For some reason, the script doesn't function properly when installed via the one-click library "Add Script" button; however, it does work if you use the "import" option. I am currently unable to identify what is causing the issue.  The problem I was able to notice was a naming and parsing issue for the Astral Planes handout from the DMG. It seems to be creating only 1 out of 3 tables and naming them all incorrectly. I tried duplicating this with the script manually added to the API sandbox and found that the problem didn't persist there. Maybe there is an issue with the JSON, but I can't identify where it might be.