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

[Sheetworkers] Script to add support for setTimeout, setInterval, and async/await.

This has been floating around in the API forum for a few weeks now, but I think it makes since to post here, now... Ever tried to use setTimeout(), or setInterval() and gotten the following error message, instead? Character Sheet Error: Trying to do getAttrs when no character is active in sandbox. Dreaming of a way to escape "callback hell" and start using JavaScript's async/await patterns? If either of these are true, this script may be for you: <a href="https://github.com/onyxring/Roll20Async" rel="nofollow">https://github.com/onyxring/Roll20Async</a> &nbsp; It has served me well in my own custom character sheets and I'm sharing it with the community in case it helps someone else. If it doesn't work quite like you expected, feel free to let me know. -Jim at OnyxRing
1601582376
Jakob
Sheet Author
API Scripter
This is really nice, thanks for sharing this find! I guess you're not getting much of a response here since most people don't know what to do with it :).
1601583985
Spren
Sheet Author
Jakob is totally right. Seems cool, but I'm not seeing a particular use case for it. Could one of you give some examples of how you have used it, or would use it?
1601585048
Andreas J.
Forum Champion
Sheet Author
Translator
Spren said: Seems cool, but I'm not seeing a particular use case for it. Could one of you give some examples of how you have used it, or would use it? Seconded. I 100% know I'm too dum-dum to understand how this could be used without an example. :D
1601587228

Edited 1601587247
Kavini
Pro
Marketplace Creator
Sheet Author
Compendium Curator
If I understand correctly this prevents the endless callback hell of sheetworkers. Instead of using a getAttrs, collecting values, and then being required to nest everything inside that callback function, this would allow you to make the function call to a variable with an await, which you would then use as any other variable, eliminating the need for nesting. I haven't responded before now, because I haven't had a chance to test it out, but if this works as advertised it'll be wonderful!
1601589070
GiGs
Pro
Sheet Author
API Scripter
I think for most sheet creators, it won't provide a benefit most of the time. It just provides a different syntax, a different way of writing your worker for the same effect. &nbsp;BUT, and this is huge, there are some workers it will make a massive difference on. The key areas that occur to me are sheet version update workers, and workers that wont to manipulate more than one repeating section at a time. These often require lots of nesting, and you can write them much cleaner and more understandably with these new functions. However, you also need to be careful: it makes it easier to write a worker with lots of getAttrs functions (as well as the other worker async functions like setAttrs), so you could easily write workers that end up very inefficient that add lag to your sheet.
1601590013
Andreas J.
Forum Champion
Sheet Author
Translator
I remember Scot C. have said more than once that either GetAttrs takes ~5ms to process while SetAttr takes ~200ms, or the other way around. Don't remember, but would document it if I did...
1601591484

Edited 1601591641
Nic B is right.&nbsp; The async/await approach is really just a (relatively) recent feature of the JavaScript language, which doesn't seem to work in Roll20.&nbsp; Without async/await/promises, it doesn't mean very much.&nbsp; Of course, fixing setInterval() and setTimeout is just a bonus, but I've seen it pop up in the forums from time to time. Truthfully, this code is a chunk of a Bigger release that I was planning on sharing, but it seems like it is self-contained enough to warrant its own posting.
1601597294
GiGs
Pro
Sheet Author
API Scripter
📜🗡Andreas J.🏹📜 said: I remember Scot C. have said more than once that either GetAttrs takes ~5ms to process while SetAttr takes ~200ms, or the other way around. Don't remember, but would document it if I did... setAttrs is the slower one (much slower). But even the 5ms of getAttrs will still add up when you have one sheet worker that causes a change in another, and another, etc. Of course, most people who use these scripts&nbsp; now&nbsp; will know well enough how to avoid that - but if it becomes popular and there are simple examples to use, people could easily end up writing very slow code, with lots of separate getAttrs calls instead of one unified one. Dont read this as discouraging use of these scripts though - I think they are amazing. Just with any powerful tool, you need to be aware of the possible pitfalls. On the speed issue, I wonder if Scott did some testing about getSectionIDs. I've often been curious about the speed of that one.
1601600577
GiGs
Pro
Sheet Author
API Scripter
On the subject of examples, here are two simple sheet workers. Note that you dont actually need to use OnyxRings scripts for these - they dont showcase the power of the scripts. They are just meant to show the difference in syntax (and how easy it is to switch to using them): For the first one: imagine you have a character sheet where the hitpoints &nbsp;attribute is calculated by adding siz &nbsp;and con . The traditional way would be like this: on('sheet:opened&nbsp;change:con&nbsp;change:siz',&nbsp;()=&gt;{ &nbsp;&nbsp;&nbsp;&nbsp;getAttrs(['con','siz'],&nbsp;values&nbsp;=&gt;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let con&nbsp;=&nbsp;parseInt(values.con)&nbsp;||&nbsp;0; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let&nbsp;siz&nbsp;=&nbsp;parseInt(values.siz)&nbsp;||&nbsp;0; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let&nbsp;hp&nbsp;=&nbsp;con&nbsp;+&nbsp;siz; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setAttrs({ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;hitpoints:&nbsp;hp &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;}); }); The above is a very familiar style of sheet worker. There are more efficient ways to write this, but I wanted to show the most common style of worker. Notice at each step where you use a roll20 function, you add an extra level of nesting. Here's what that looks like with OnyxRing's scripts: on('sheet:opened&nbsp;change:con&nbsp;change:siz',&nbsp;async&nbsp;()=&gt;{ &nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;values&nbsp;=&nbsp;await&nbsp;getAttrsAsync(['siz','con']); &nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;con&nbsp;=&nbsp;+parseInt(values.con)&nbsp;||&nbsp;0; &nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;siz&nbsp;=&nbsp;+parseInt(values.siz)&nbsp;||&nbsp;0; &nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;hp&nbsp;=&nbsp;con&nbsp;+&nbsp;siz; &nbsp;&nbsp;&nbsp;&nbsp;setAttrs({ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;hitpoints:&nbsp;hp &nbsp;&nbsp;&nbsp;&nbsp;}); }); Notice there's no nesting. You write the worker in a simple progression. You just have to add the words async and await at certain points. Here's a more complex example, working on a repeating section. Imagine you have a repeating section of different types of armour. Each piece has an armor value and an armor worn checkbox. If it's worn you add the armour to the total, and then update a totalarmor &nbsp;attribute. That sheet worker might look like this: on(`change:repeating_armor`,&nbsp;()&nbsp;=&gt;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;getSectionIDs('repeating_armor',&nbsp;id_array&nbsp;=&gt;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;armor&nbsp;=&nbsp;[]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;id_array.forEach(id&nbsp;=&gt;&nbsp;armor.push( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`repeating_armor_${id}_armorbonus`, &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`repeating_armor_${id}_armorworn` &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;getAttrs(armor,&nbsp;values&nbsp;=&gt;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let&nbsp;tarm&nbsp;=&nbsp;0; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;id_array.forEach(id&nbsp;=&gt;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;bonus&nbsp;=&nbsp;parseInt(values[`repeating_armor_${id}_armorbonus`])&nbsp;||&nbsp;0; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;worn&nbsp;=&nbsp;parseInt(values[`repeating_armor_${id}_armorworn`])&nbsp;||&nbsp;1; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(worn)&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tarm&nbsp;+=&nbsp;bonus; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setAttrs({ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;totalarmor:&nbsp;tarm &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;}); }); So, you first need to use getSectionIDs, and then put the rest of the worker inside that. Then use getAttrs, and put the rest of the worker inside that. With OR's scripts, that would look like the code below. It's still pretty complex, but might be more readable since you have a nice linear flow. on(`change:repeating_armor`,&nbsp;async&nbsp;()&nbsp;=&gt;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;id_array&nbsp;=&nbsp;await&nbsp;getSectionIDsAsync('repeating_armor'); &nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;armor&nbsp;=&nbsp;[]; &nbsp;&nbsp;&nbsp;&nbsp;id_array.forEach(id&nbsp;=&gt;&nbsp;armor.push( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`repeating_armor_${id}_armorbonus`, &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`repeating_armor_${id}_armorworn` &nbsp;&nbsp;&nbsp;&nbsp;)); &nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;values&nbsp;=&nbsp;await&nbsp;getAttrsAsync(armor); &nbsp;&nbsp;&nbsp;&nbsp;let&nbsp;tarm&nbsp;=&nbsp;0; &nbsp;&nbsp;&nbsp;&nbsp;id_array.forEach(id&nbsp;=&gt;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;bonus&nbsp;=&nbsp;+values[`repeating_armor_${id}_armorbonus`]&nbsp;||&nbsp;0; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;worn&nbsp;=&nbsp;+values[`repeating_armor_${id}_armorworn`]&nbsp;||&nbsp;1; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(worn)&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tarm&nbsp;+=&nbsp;bonus; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;setAttrs({ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;totalarmor:&nbsp;tarm &nbsp;&nbsp;&nbsp;&nbsp;}); }); To really show the power of these scripts though, you need to be using more complex workers - once that use setAttrs and then do things after it, say, or ones which work on multiple repeating sections (I have done one version update script that worked on something like 6 repeating sections, and i needed to nest each one inside the next - being able to just write out a chain of 6 getSectionIDsAsyc in a vertical line would have made a much more readable worker). But I hope these examples show the basics.
1601615129
GiGs
Pro
Sheet Author
API Scripter
Here's a more complex example. Imagine you have a system which has spells that go from level 1 to 9, and you have set up a different repeating section for each spell level. I've seen at least one sheet set up this way, so its not an unrealistic example. Now, the spells need to have a bonus calculated, and that is based on the stats int, wis, or cha. So every time one of those 3 stats change, your workers needs to check all spells to see which spell bonuses have changed. The sheet worker for level 1 spells might look like this: on ( `change:repeating_spells01&nbsp;change:int&nbsp;change:wis&nbsp;change:cha` ,&nbsp;()&nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; statsallowed &nbsp;=&nbsp;[ 'int' ,&nbsp; 'wis' ,&nbsp; 'cha' ]; &nbsp;&nbsp;&nbsp;&nbsp; getSectionIDs ( 'repeating_spells01' ,&nbsp; id_array &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; spells &nbsp;=&nbsp;[]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; id_array . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells01_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; getAttrs ([... spells ,&nbsp;... statsallowed ],&nbsp; values &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; let &nbsp; output &nbsp;=&nbsp;{}; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; id_array . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells01_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells01_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; setAttrs ( output ); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;}); }); Explaining the forEach loop inside the getAttrs: each spell has a stat linked to it, and that name of that stat is stored in an attribute within the repeating section. Then the worker gets the value of the stat using that name, and stores it in the spellmodifer attribute for that stat. So far so good. The problem is, you need another worker for each level of spell. These means every time one of the 3 stats, you are doing 9 getAttrs and 9 setAttrs operations. That is very inefficient. One solution for this would be to convert it to a single worker that does all 9 spells at once. Unfortunately that looks something like this: on ( 'change:repeating_spells01&nbsp;change:repeating_spells02&nbsp;change:repeating_spells03&nbsp;change:repeating_spells04&nbsp;change:repeating_spells05' &nbsp;+&nbsp; 'change:repeating_spells06&nbsp;change:repeating_spells07&nbsp;change:repeating_spells08&nbsp;change:repeating_spells09&nbsp;change:int&nbsp;change:wis&nbsp;change:cha' ,&nbsp;()&nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; statsallowed &nbsp;=&nbsp;[ 'int' ,&nbsp; 'wis' ,&nbsp; 'cha' ]; &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; spells &nbsp;=&nbsp;[]; &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; output &nbsp;=&nbsp;{}; &nbsp;&nbsp;&nbsp;&nbsp; getSectionIDs ( 'repeating_spells01' ,&nbsp; ids_level01 &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level01 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells01_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; getSectionIDs ( 'repeating_spells02' ,&nbsp; ids_level02 &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level02 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells02_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; getSectionIDs ( 'repeating_spells03' ,&nbsp; ids_level03 &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level03 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells03_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; getSectionIDs ( 'repeating_spells04' ,&nbsp; ids_level04 &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level04 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells04_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; getSectionIDs ( 'repeating_spells05' ,&nbsp; ids_level05 &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level05 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells05_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; getSectionIDs ( 'repeating_spells06' ,&nbsp; ids_level06 &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level06 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells06_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; getSectionIDs ( 'repeating_spells07' ,&nbsp; ids_level07 &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level07 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells07_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; getSectionIDs ( 'repeating_spells08' ,&nbsp; ids_level08 &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level08 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells08_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; getSectionIDs ( 'repeating_spells09' ,&nbsp; ids_level09 &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level09 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells09_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; getAttrs ([... spells ,&nbsp;... statsallowed ],&nbsp; values &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level01 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells01_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells01_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level02 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells02_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells02_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level03 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells03_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells03_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level04 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells04_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells04_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level05 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells05_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells05_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level06 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells06_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells06_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level07 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells07_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells07_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level08 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells08_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells08_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ids_level09 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells09_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells09_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; setAttrs ( output ); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;}); }); Caveat: there are more efficient ways to write this (especially to avoid the 9 forEach loops inside getAttrs) - but you cant avoid doing the 9 getSectionIDs, and thats the main point of this example.&nbsp; Writing - and reading - sheet workers like that is no fun. With OnyxRing's scripts, this can be rewritten as: on ( 'change:repeating_spells01&nbsp;change:repeating_spells02&nbsp;change:repeating_spells03&nbsp;change:repeating_spells04&nbsp;change:repeating_spells05' &nbsp;+&nbsp; 'change:repeating_spells06&nbsp;change:repeating_spells07&nbsp;change:repeating_spells08&nbsp;change:repeating_spells09&nbsp;change:int&nbsp;change:wis&nbsp;change:cha' ,&nbsp; async &nbsp;()&nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; statsallowed &nbsp;=&nbsp;[ 'int' ,&nbsp; 'wis' ,&nbsp; 'cha' ]; &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; spells &nbsp;=&nbsp;[]; &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; output &nbsp;=&nbsp;{}; &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; ids_level01 &nbsp;=&nbsp; await &nbsp; getSectionIDsAsync ( 'repeating_spells01' ); &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; ids_level02 &nbsp;=&nbsp; await &nbsp; getSectionIDsAsync ( 'repeating_spells02' ); &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; ids_level03 &nbsp;=&nbsp; await &nbsp; getSectionIDsAsync ( 'repeating_spells03' ); &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; ids_level04 &nbsp;=&nbsp; await &nbsp; getSectionIDsAsync ( 'repeating_spells04' ); &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; ids_level05 &nbsp;=&nbsp; await &nbsp; getSectionIDsAsync ( 'repeating_spells05' ); &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; ids_level06 &nbsp;=&nbsp; await &nbsp; getSectionIDsAsync ( 'repeating_spells06' ); &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; ids_level07 &nbsp;=&nbsp; await &nbsp; getSectionIDsAsync ( 'repeating_spells07' ); &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; ids_level08 &nbsp;=&nbsp; await &nbsp; getSectionIDsAsync ( 'repeating_spells08' ); &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; ids_level09 &nbsp;=&nbsp; await &nbsp; getSectionIDsAsync ( 'repeating_spells09' ); &nbsp;&nbsp;&nbsp;&nbsp; ids_level01 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells01_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp; ids_level02 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells02_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp; ids_level03 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells03_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp; ids_level04 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells04_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp; ids_level05 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells05_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp; ids_level06 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells06_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp; ids_level07 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells07_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp; ids_level08 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells08_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp; ids_level09 . forEach ( id &nbsp; =&gt; &nbsp; spells . push ( `repeating_spells09_ ${ id } _statbonus` )); &nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; values &nbsp;=&nbsp; await &nbsp; getAttrsAsync ([... spells ,&nbsp;... statsallowed ]); &nbsp;&nbsp;&nbsp;&nbsp; ids_level01 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells01_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells01_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp; ids_level02 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells02_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells02_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp; ids_level03 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells03_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells03_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp; ids_level04 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells04_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells04_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp; ids_level05 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells05_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells05_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp; ids_level06 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells06_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells06_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp; ids_level07 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells07_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells07_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp; ids_level08 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells08_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells08_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp; ids_level09 . forEach ( id &nbsp; =&gt; &nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const &nbsp; stat &nbsp;=&nbsp; values [ `repeating_spells09_ ${ id } _statbonus` ]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; output [ `repeating_spells09_ ${ id } _spellmodifier` ]&nbsp;=&nbsp; values [ stat ]; &nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp; setAttrs ( output ); }); The 9 forEach loops at the end are still a bit clunky, and that code could be rewritten, but I wanted to avoid any more tricky techniques to keep the workers as simple to understand as possible.&nbsp; You should be able to see at a glance though how much more readable this code is compared to the previous worker.
1601615597
GiGs
Pro
Sheet Author
API Scripter
Finally, here's an example from OnyxRings githib: on("sheet:opened", async ()=&gt;{ var values = await getAttrsAsync(["hp","rec"]); var newHp=Number(values.hp||0) + Number(values.rec||0); await setAttrsAsync({hp:newHp}); values = await getAttrsAsync(["hp"]); console.assert(values.hp==newHp, "Failed"); }); This is calculating a HP score, saving it to the sheet. And then after setAttrs has run to set the value, it checks to see if the value on the sheet has been updated correctly. This kind of thing is a pain to do normally. Wanting to do things after setting sheet values takes you to callback hell. This is going to make sheet versioning sheet workers a lot easier to write.
@GiGs: &nbsp;Thanks for posting these examples. &nbsp;It’s pretty clear my initial post on this could have been more thorough in this regard. &nbsp;&nbsp;
1601651063
Peter B.
Plus
Sheet Author
GiGs said: Finally, here's an example from OnyxRings githib: on("sheet:opened", async ()=&gt;{ var values = await getAttrsAsync(["hp","rec"]); var newHp=Number(values.hp||0) + Number(values.rec||0); await setAttrsAsync({hp:newHp}); values = await getAttrsAsync(["hp"]); console.assert(values.hp==newHp, "Failed"); }); This is calculating a HP score, saving it to the sheet. And then after setAttrs has run to set the value, it checks to see if the value on the sheet has been updated correctly. This kind of thing is a pain to do normally. Wanting to do things after setting sheet values takes you to callback hell. This is going to make sheet versioning sheet workers a lot easier to write. Is async/await available now in sheets or is it an update that is scheduled to come later for sheets in the future?
1601651270
GiGs
Pro
Sheet Author
API Scripter
Peter B. said: Is async/await available now in sheets or is it an update that is scheduled to come later for sheets in the future? It's a feature added by the functions OnyxRings adds in the OP. It's not a feature roll20 has added, you need these functions to enable them.
Async/await as a syntax is part of the JavaScript language. It’s available to use, but doesn’t work quite right in vanilla Roll20. Instead, without the script, Roll20 generates the error described above.&nbsp;
1601668513
GiGs
Pro
Sheet Author
API Scripter
Async/Await work fine in the API. It's just in sheet workers where they don't work, and specifically on functions that require a character context - getAttrs, setAttrs, getSectionIDS - exactly the functions that OR replaces here. You can use async/await in your own functions (as OR has here, in fact). I believe its just the previously named functions that interface with roll20's backend that lose the character context if you try to use async/await with them.&nbsp;
1601676469
Spren
Sheet Author
I honestly really like this now that I've seen it. Thanks for taking the time and doing this Onyx, and thank you GiGs for the really good explanations and examples.
Good clarification, GiGs.&nbsp; Thanks!&nbsp; Spren: I hope this works out for you.&nbsp;
1601702367
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Awesome work Onyx! GiGs, I didn't do any testing with getSectionIDs, mostly cause I just didn't think of it. Onyx, a question. In your code, I see a function call to&nbsp; getActiveCharacterId() , but no definition of that function. Is that a function you created? or is it a Roll20 function? It's not documented in the sheetworker docs, so is it a backend function that has been erroneously exposed? I ask, cause while I'm ecstatic to see async/await/promises come to sheetworkers, I'm actually more excited about the prospect of getting the active character's id. Being able to get the ID would allow me to do some better indexing with the trick that allows pseudo interaction between separate sheets. And, even more exciting, if I understand your code correctly it may actually allow us to manipulate connected sheets when an event occurs on one of them. That's just based on my first pass of reading the code, but if true it would be a game changer for a wide array of systems.
Onyx, a question. In your code, I see a function call to&nbsp; getActiveCharacterId() , but no definition of that function. Is that a function you created? or is it a Roll20 function? It's not documented in the sheetworker docs, so is it a backend function that has been erroneously exposed? It is a Roll20 function; getActiveCharacterId() is built into the SheetWorker code.&nbsp; As far as being "erroneously" exposed... It&nbsp; feels&nbsp; &nbsp;to me like it was exposed with intention.&nbsp; Still, like you, my Spidey-sense tingles a bit because a couple of the tricks I use are undocumented.&nbsp; I actually dedicated a sidebar on the topic in the docs for ORAL ORCS , page 16.&nbsp; (Yeah I know, shameless plug).
1601743193
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Huh, thanks for the details Onyx. I'm gonna have to play around with this, but I've got some ideas of ways to use that.
1601752065

Edited 1701362148
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Ok, so with what Onyx has found and put together here, we can do something via character sheets which has previously been the sole purview of the API. This alone may make Roll20 close these functions off, but here goes. Because we can now get the active character's ID, and change what the active character is (by changing the id of the active character), we can now affect other sheets based on a change on another sheet: Those eagle eyed watchers will note that to clear the received field, I actually put a space in. This is because the attribute backed span that I'm using for the received value doesn't clear when set to "". Here's the code I'm using to do this (including Onyx's original Roll20Async code): &lt;input type='text' title='@{character_id}' name='attr_character_id' value=''&gt; &lt;input type='text' title='@{target_id}' name='attr_target_id' value=''&gt; &lt;input type='text' title='@{shared_value}' name='attr_shared_value' value=''&gt; &lt;span&gt;Received Value:&lt;/span&gt;&lt;input type='readonly' name='attr_received_value'&gt; &lt;script type='text/worker'&gt; &nbsp; &nbsp; function setActiveCharacterId(charId){ &nbsp; &nbsp; &nbsp; &nbsp; var oldAcid=getActiveCharacterId(); &nbsp; &nbsp; &nbsp; &nbsp; var ev = new CustomEvent("message"); &nbsp; &nbsp; &nbsp; &nbsp; ev.data={"id":"0", "type":"setActiveCharacter", "data":charId}; &nbsp; &nbsp; &nbsp; &nbsp; self.dispatchEvent(ev);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return oldAcid; &nbsp; &nbsp; }; &nbsp; &nbsp; var _sIn=setInterval; &nbsp; &nbsp; setInterval=function(callback, timeout){ &nbsp; &nbsp; &nbsp; &nbsp; var acid=getActiveCharacterId(); &nbsp; &nbsp; &nbsp; &nbsp; _sIn( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; function(){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var prevAcid=setActiveCharacterId(acid); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; callback(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; setActiveCharacterId(prevAcid); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; ,timeout); &nbsp; &nbsp; }; &nbsp; &nbsp; var _sto=setTimeout &nbsp; &nbsp; setTimeout=function(callback, timeout){ &nbsp; &nbsp; &nbsp; &nbsp; var acid=getActiveCharacterId(); &nbsp; &nbsp; &nbsp; &nbsp; _sto( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; function(){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var prevAcid=setActiveCharacterId(acid); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; callback(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; setActiveCharacterId(prevAcid); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; ,timeout); &nbsp; &nbsp; }; &nbsp; &nbsp; function getAttrsAsync(props){ &nbsp; &nbsp; &nbsp; &nbsp; var acid=getActiveCharacterId(); //save the current activeCharacterID in case it has changed when the promise runs&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var prevAcid=null;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//local variable defined here, because it needs to be shared across the promise callbacks defined below &nbsp; &nbsp; &nbsp; &nbsp; return new Promise((resolve,reject)=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; prevAcid=setActiveCharacterId(acid);&nbsp; //in case the activeCharacterId has changed, restore it to what we were expecting and save the current value to restore later &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; getAttrs(props,(values)=&gt;{&nbsp; resolve(values); });&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; catch{ reject(); } &nbsp; &nbsp; &nbsp; &nbsp; }).finally(()=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; setActiveCharacterId(prevAcid); //restore activeCharcterId to what it was when the promise first ran &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; }; &nbsp; &nbsp; //use the same pattern for each of the following... &nbsp; &nbsp; function setAttrsAsync(propObj, options){ &nbsp; &nbsp; &nbsp; &nbsp; var acid=getActiveCharacterId();&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var prevAcid=null;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return new Promise((resolve,reject)=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; prevAcid=setActiveCharacterId(acid);&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; setAttrs(propObj,options,(values)=&gt;{ resolve(values); }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; catch{ reject(); } &nbsp; &nbsp; &nbsp; &nbsp; }).finally(()=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; setActiveCharacterId(prevAcid);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; }; &nbsp; &nbsp; function getSectionIDsAsync(sectionName){ &nbsp; &nbsp; &nbsp; &nbsp; var acid=getActiveCharacterId();&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var prevAcid=null;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return new Promise((resolve,reject)=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; prevAcid=setActiveCharacterId(acid);&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; getSectionIDs(sectionName,(values)=&gt;{ resolve(values); }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; catch{ reject(); } &nbsp; &nbsp; &nbsp; &nbsp; }).finally(()=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; setActiveCharacterId(prevAcid);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; }; &nbsp; &nbsp; function log(msg,obj){ &nbsp; &nbsp; &nbsp; &nbsp; const sheetName = 'Test Sheet'; &nbsp; &nbsp; &nbsp; &nbsp; if(typeof msg === 'string'){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; console.log(`%c${sheetName} log| ${msg}`,"background-color:#159ccf"); &nbsp; &nbsp; &nbsp; &nbsp; }else{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; console.log(`%c${sheetName} log| ${typeof msg}`,"background-color:#159ccf"); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; console.log(msg); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if(obj){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; console.log(obj); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; console.log(`%c==============`,"background-color:#159ccf"); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }; &nbsp; &nbsp; on('sheet:opened',()=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; const setObj = {}; &nbsp; &nbsp; &nbsp; &nbsp; setObj.character_id = getActiveCharacterId(); &nbsp; &nbsp; &nbsp; &nbsp; setAttrs(setObj); &nbsp; &nbsp; }); &nbsp; &nbsp; on('change:shared_value',async (event)=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; log(event); &nbsp; &nbsp; &nbsp; &nbsp; let attributes = await getAttrsAsync(['character_id','target_id']); &nbsp; &nbsp; &nbsp; &nbsp; if(!attributes.target_id || attributes.target_id === ''){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; const sourceID = attributes.character_id, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; setObj ={}; &nbsp; &nbsp; &nbsp; &nbsp; let targetID = setActiveCharacterId(attributes.target_id); &nbsp; &nbsp; &nbsp; &nbsp; setObj.received_value =event.newValue === undefined ? '' : event.newValue; &nbsp; &nbsp; &nbsp; &nbsp; log(setObj); &nbsp; &nbsp; &nbsp; &nbsp; await setAttrsAsync(setObj); &nbsp; &nbsp; &nbsp; &nbsp; setActiveCharacterId(sourceID); &nbsp; &nbsp; }); &nbsp; &nbsp; log('sheetworker loaded'); &lt;/script&gt; So, thanks for this Onyx!
1601755000
GiGs
Pro
Sheet Author
API Scripter
Intriguing. i also noticed that getActiveCharacterId function and wondered if we'd be able to do something like this.
1601850506

Edited 1601911169
@scott: &nbsp; this is really cool. &nbsp;I particularly like how you demoed it as GIF, which really drives home what it can do!
1601877906
Jakob
Sheet Author
API Scripter
And thus get/setActiveCharacterId became part of the public API :).