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

ORAL ORCS a framework for Roll20

1601683896

Edited 1601684101
I’ve really enjoyed working in Roll20 since I stumbled across it and I love the community, which I’ve found helpful, knowledgeable, and every bit as excited about gaming as I am.&nbsp; As I’ve been porting my campaigns and building custom character sheets, I’ve structured my work into two groups: that which is game-specific and that which is generic and reusable.&nbsp; I wanted to share the latter of these with the community, in the form of a project I call “ORAL ORCS”: &nbsp; <a href="https://github.com/onyxring/Roll20OralOrcs" rel="nofollow">https://github.com/onyxring/Roll20OralOrcs</a> &nbsp; ORAL ORCS is a collection of scripts which work together, a framework really.&nbsp; It’s easy to install, has a footprint which resides in both the API and Workersheet spaces, and adds support for modern features of the JavaScript language.&nbsp; Contrary to the name, it is not genre-specific and has nothing to do with orcs.&nbsp; Or dentists.&nbsp; &nbsp; Like many frameworks, it abstracts underlying services, simplifying them.&nbsp; I believe it makes code easier to write, easier to read, and easier to understand.&nbsp; By design, the framework is inert:&nbsp; it can be added to an existing game with no effect until its features are deliberately used.&nbsp; &nbsp; I’ve put together an introductory guide/tutorial, in which I try to strike a balance between technical content and readability: &nbsp; <a href="https://github.com/onyxring/Roll20OralOrcs/blob/master/OralOrcs.pdf" rel="nofollow">https://github.com/onyxring/Roll20OralOrcs/blob/master/OralOrcs.pdf</a> &nbsp; Outside of the above document, a few examples are in order for this announcement.&nbsp; These don’t equate to an exhaustive list of features, but they should provide a taste of what is provided, assuming readers are familiar with how to do these things in vanilla Roll20 for contrast: &nbsp; 1. The framework makes the act of manipulating character attributes as simple as manipulating properties.&nbsp; Here we add a character’s recovery attribute to his or her hitPoints attribute:&nbsp; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pc.hitPoints = pc.hitPoints + pc.recovery; &nbsp; Note that calls to the traditional getAttrs/setAttrs functions are abstracted away from your code. &nbsp; 2. Working with Repeating Sections is also more intuitive.&nbsp; Below, we cycle through a character’s list of spells, totaling their collective “attack” power and removing the spells that have been marked as used up/expended: &nbsp; var totAtkPower = 0; pc.repeating.spells.forEach(spell=&gt;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if(spell.isAttack=="on") totAtkPower = totAtkPower + spell.power; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if(spell.isExpended=="on") spell.delRow(); }); &nbsp; Here, there is no need to make calls to getSectionIds() and we read the attributes of spells in the same fashion we did character attributes in the previous example.&nbsp; We can also write to these in the same way.&nbsp; &nbsp; The delRow() call removes individual items.&nbsp; Similarly… &nbsp; var newSpell = pc.repeating.spells.addNew(); &nbsp; …will create a new entry in the Repeating Section. &nbsp; 3. &nbsp;ORAL ORCS includes a simplified approach to creating macro/chat processors:&nbsp; &nbsp; class api extends orProcessor{ &nbsp;&nbsp;&nbsp; constructor(){ super("!API"); &nbsp;} &nbsp;&nbsp;&nbsp; time(){ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;orSend.all(`Current Time:${new Date().toUTCString()}`); &nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp; hello(){ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; orSend.all(`Right back at you.`); &nbsp;&nbsp;&nbsp; } } new api(); &nbsp; Parsing of chat text is done for you.&nbsp; The above is all that is required to begin processing !API commands.&nbsp; For demonstration, it also creates two simple commands that will trigger when the correct text is placed in chat: &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !API time and &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !API hello &nbsp; &nbsp; In addition to the above examples, the framework provides a recursive template system and custom dice rolling.&nbsp; It includes the recently released AsyncRoll20 script, so the native functions setTimeout() and setInterval() now work correctly in Sheetworkers.&nbsp; The traditional API function sendChat() has been brought into Sheetworkers as well. &nbsp; Today, the framework is nascent, with a version number less than 1, and I’m confident that it is NOT bug-free.&nbsp; Quite the contrary.&nbsp; I have, however, been using it for my personal projects, so there has been a measure of stabilization already. &nbsp; I have had, and continue to have, a lot of satisfaction building ORAL ORCS and I hope other creators might find it useful.&nbsp; From those that are inclined, I’d appreciate feedback on how to make ORAL ORCS more helpful, easier to use, and/or the guide easier to read. &nbsp; Cheers, &nbsp; Jim at OnyxRing &nbsp; &nbsp;
1601748466

Edited 1601748532
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
I take it this is the full project you alluded to in your earlier post in the sheets forum. It certainly looks interesting, however I've got a few questions: Where is the ORCSCode that your initial on('sheet:opened') gets located? and what's contained in that code? How are you adding sendChat to character sheets? I'm assuming this is contained in ORCSCode somewhere, but not sure what you're doing exactly And, what is the web worker function call that you reference in your side bar on the second to last page of the pdf?
&nbsp; I've got a few questions: Where is the ORCSCode that your initial on('sheet:opened') gets located? The OnyxRing Client Script (ORCS) is actually embedded in the "API Library" (ORAL) half of the framework.&nbsp; This side-steps the whole versioning conundrum:&nbsp; Update the API-side and the worker side automatically updates when the browser is refreshed. an d what's contained in that code? That line downloads all of client-side code for the framework.&nbsp; This includes the Async/Await code that we've discussed on a different thread, as well as all the objects described in the latter half of the document (orCharacter, orTemplate, orSend, etc...).&nbsp; It is all checked into the GitHub repository as separate files which may be a bit easier to peruse. How are you adding sendChat to character sheets? I'm assuming this is contained in ORCSCode somewhere, but not sure what you're doing exactly Nothing too difficult here.&nbsp; This is just a server-side on-change event handler which acts as a proxy:&nbsp; ORCS loads the dedicated attribute with the message, ORAL pulls it off and sends it to the chat.&nbsp; To the users of the framework, this is presented as the familiar looking function "sendChat". And, what is the web worker function call that you reference in your side bar on the second to last page of the pdf I was referring to this: <a href="https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent" rel="nofollow">https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent</a>
1601752083
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Ah, ok, so a character sheet that relied on ORCS wouldn't function without ORAL being installed on the API console. That means that the sheet would be API dependent, which makes this a little difficult to use for projects that are planned for widespread use and inclusion in the Roll20 repo/sheet drop down. However, I've found something really fun to do with what you put together in your Roll20 Async functions. I'll discuss that over in that thread .
1601852307
GiGs
Pro
Sheet Author
API Scripter
I havent had time to look at the code (and might not understand it when I do, haha), but I have questions about how the sheet worker side works. You posted this example: var totAtkPower = 0; pc.repeating.spells.forEach(spell=&gt;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if(spell.isAttack=="on") totAtkPower = totAtkPower + spell.power; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if(spell.isExpended=="on") spell.delRow(); }); following a similar example using non-repeating attributes. I assume these are using the roll20 functions behind the scenes (getAttrs, getSectionIDs, setAttrs). Is that correct? Is every attribute reference a separate getAttrs call, or does the script have some way of bundling up and queueing separate calls to do them all at once, to handle the potential lag from multiple calls to roll20s backend?
Nothing quite as sophisticated as that. &nbsp;As you suspect, these are individual calls, doing the round-trip for each. &nbsp;I have been toying with multiple attribute features in orCharacter for a future release, but the&nbsp; syntax &nbsp;presented to the developer is something that I’m still thinking through. BTW: The examples given above are API examples. &nbsp;There are Workersheet equivalents to these, but they still leverage the async/await syntax.
1601914103

Edited 1601914281
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Ah, if each of those calls causes a getAttr/setAttr, this is going to cause some serious lag on even moderately complex sheets. You could quite easily wind up with a second of lag in changes being applied to the sheet just by changing 5 or so attributes. Throw in a change that adjusts a bunch of repeating items and you're up to several seconds of lag very easily. Now a second of lag might not get noticed that much, and really isn't that bad, but start changing 10, 20, 30, or more attributes and suddenly you're looking at sizable fractions of a minute before changes propagate to the sheet. That starts to be a usability issue as the user is then able (and even likely) to edit some other attribute that affects the same attribute that has a change waiting before that change takes effect, and then you wind up with calculations being off because the two changes wind up in a race against each other. What I might suggest, since you've already cracked the question of how to add asynchronous functions to character sheets is to create a character sheet safe version of the debounce function. That way you could aggregate all changes for a character and apply them at once. This wouldn't solve the problem of multiple getAttrs, but would at least reduce the number of setAttr calls (and that's the real lag producer). You'd also need to ensure that calls from multiple sheets don't get saved together, but I'm sure that's something that can be figured out.
1601915812
GiGs
Pro
Sheet Author
API Scripter
Scott C. said: Ah, if each of those calls causes a getAttr/setAttr, this is going to cause some serious lag on even moderately complex sheets. You could quite easily wind up with a second of lag in changes being applied to the sheet just by changing 5 or so attributes. Throw in a change that adjusts a bunch of repeating items and you're up to several seconds of lag very easily. Now a second of lag might not get noticed that much, and really isn't that bad, but start changing 10, 20, 30, or more attributes and suddenly you're looking at sizable fractions of a minute before changes propagate to the sheet.&nbsp; And to put Scott's numbers into perspective: Remember that changing just one attribute can propagate changes to dozens of other attributes, some of which may trigger their own cascades.&nbsp;
1601939804

Edited 1601940713
I like the use of setTimeout/Interval in concept, but I think combining it with the async/await constructs and waiting for cascading change handlers may result in complexity of code and side-effects.&nbsp; I'll think through it and maybe dabble a bit with the idea.&nbsp; I was leaning toward a conscious choice to use bulk or single attribute access, retaining the conceptual approach that the Roll20 system uses, but with a cleaner syntax.&nbsp; Something like the following "cache and release" syntax: //Note: "damage" is an input variable //&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "ca" is the theoretical cachedAttributes object var ca = await pc.cache(["body","con","pd", "isStunned"]); &nbsp; &nbsp; var dmgDone = (damage - ca.pd);&nbsp; //subtract physical defense from damage &nbsp;&nbsp;&nbsp;&nbsp;ca.body = ca.body - dmgDone; &nbsp; &nbsp; ca.isStunned=dmgDone &gt; ca.con; //character is stunned if dmg is greater than constitution &nbsp; &nbsp; await ca.release(); //or "commit" ;) This would essentially translate into: Get copies of a bunch of attributes in one call via getAttrs() Read and change the copies of several attributes, no event handlers would fire Release the cache by writing all the changed attributes in one call to setAttrs(), event handlers firing This would preserve the existing single attribute syntax in the framework, and even allow the developer to compare cached values to current values in case some API event triggered a change outside of the worker: &nbsp;&nbsp;&nbsp;&nbsp;assert(ca.body==await pc.body, ...); Expanding it to include repeatingSections might be a little tricky in code, but entirely possible.&nbsp; Would definitely be a .2 version of the framework unless I'm missing something in the approach. Thoughts?
1601944668
GiGs
Pro
Sheet Author
API Scripter
That would be better. The perfect system to me would worlk like this (and I expect its not possible): The first time you Get an attribute value, it actually grabs all the attributes associated with the current character and stores them in an object. Not just the attributes named - but every single one.&nbsp; Any future gets grab the values from the stored character object. Any setAttrs commands just update this character object and dont sync back to the database. You sync back to the database on two occasions: On a timer - every couple of seconds, a check is made: if any attributes have been Set, a synch is performed - all attributes that have been changed are synched back to the server And also, if a Get is performed on an attribute that has been Set since the last sync, a new sync is performed. This is to make sure everything is kept in sync This would pretty much eliminate lag, if it was possible to do. I'm operating on the assumption using Get on many attributes doesnt take much longer than a Get on one attribute - that might not be accurate.&nbsp; I dont think it's possible to grab all attributes without expressly listing them all, though. Failing that pipedream, your suggested approach of bundling gets (and sets!) just like you can with normal sheet workers would be good.
1601948717
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Onyx, your cache method is pretty much how I recommend doing it right now. GiGs, your dream scenario is also a little bit how I do sheetworkers right now. While it isn't possible to get all attributes on a character automatically, I use a variable at the start of my sheetworkers that has the name of every attribute I might possibly need for any event. Then I just do a .forEach on(change) and call a generic sheet function that does logic to determine what to change. I can confirm that getAttrs takes the same time for 1 attribute or 1000 (yes, I've actually tested this). The big problem I see with your sync dream scenario GiGs is if multiple people access a sheet, keeping the all the iterations of the sheetworker (not to mention the API sandbox iteration of the sheetworker) in sync would be a little interesting. I'm also not sure it would actually reduce lag. The big lag producer is setAttrs after all, not the initial getAttrs to react to a change.
1601954107

Edited 1602005509
GiGs
Pro
Sheet Author
API Scripter
Scott C. &nbsp;said: GiGs, your dream scenario is also a little bit how I do sheetworkers right now.&nbsp; I know, but its a lot of work building all the code to manage everything that way. It makes the code much more convoluted, and harder for people to maintain if they arent the original sheet's creator and thus have an intimate knowledge of how everything fits together. A universal automatic solution would be great :) The big problem I see with your sync dream scenario GiGs is if multiple people access a sheet Great point, I hadnt considered that. I dont know if I explained it properly, but my dream sceneario was designed to cut down on the number of setAttrrs massively - basically bundling all setAttrs together and only writing them to the server at an interval which would include a bunch of non-clashing setAttrs events, and only doing it early if a getAttrs is called to an attribute that has a setAtrs queued for it.&nbsp; But I forgot to consider multiple people using the sheet, which changes things and probably makes it completely impractical.
1601956955
The Aaron
Roll20 Production Team
API Scripter
Caching and a single set is how TheAaronSheet works. Since it all takes place in a controlled chain, there is a known endpoint when the changes can be committed. It maintains a list of changed attributes, providing the last set value to anyone accessing the attributes during the chain, then doing a single set of all changes in one go.&nbsp;
Yes.&nbsp; It does seem like controlling the scope/life of the cache is important.&nbsp; My own work has a combat system built into it where API code determines damage and, after GM confirmation, applies the result to the target's attributes.&nbsp; Without regular refreshes, these changed attributes would be overwritten by their previously cached versions.&nbsp; The same would apply to ScottC's cross sheet demo that he provided on an adjacent thread.