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

[Help] Optimizing Modification Section

Goal Create a modification system that makes it easy for users to implement sheet-wide changes with no limit to the number of attributes affected in any one change and provide accurate user feedback to ensure that settings are changed appropriately. Setup Repeating Section "Modifications": This repeating section contains entries that directly alter attributes, such as increasing strength by 5, or doubling maximum hit points, or setting your darkvision distance.  Each of these entries has a user-assigned "key" that is not necessarily unique.  It also contains a toggle for turning the bonus on or off. Repeating Section "Modification Groups": This repeating section contains a toggle, name, and a key from the Modifications section.  When toggled, all modifications that share the key specified are toggled to match the same state.  For example, turning off your Werewolf form would mean setting every Modification to false. Repeating Section "Modification Validations": This repeating section allows users to set "Linked", "Required", or "Exclusive" settings for multiple modification groups.  When modification group settings violate these constraints, this rule and the offending modification groups are highlighted in red. Example Workflow A user (User) gains a Ring of Strength +4, and already has a Ring of Darkvision and a Ring of Regeneration. User enters a new Modification in the Modification Repeating Section: { 'key': 'ring-of-strength', 'name': 'Ring of Strength: Strength Bonus', 'attribute': 'strength', 'value': 4, 'operation': 'modify' } The sheet worker creates a new "Modification Group" record because this is a new key value in the Modification Section. { 'key': 'ring-of-strength', 'name': 'Ring of Strength', 'enabled': 0 } The user copies the key 'ring-of-strength' and adds it to his or her existing '2 Ring Maximum' validation rule: { 'keys': 'ring-of-regeneration|ring-of-darkvision|ring-of-strength', 'name':'2 Ring Maximum', 'rule': 'exclusive', 'value': 2 } When they turn on their "Ring of Strength" modification group, it should then turn red, as should the "Ring of Regeneration" and "Ring of Darkvision" modification groups and the "2 Ring Maximum" validation rule.  When they turn off at least one of the other rings, everything should return to normal. The user also creates a "Linked" validation rule, provides the 'equipment-rings' repeating section name, the 'left-hand-equipped' and 'right-hand-equipped' attributes, the 'ring-of-strength' key, and gives it the rowId of the row where he added the Ring of Strength item to his character sheet. When the user equips or unequips the ring of strength from the rings section of his character sheet, the modification is automatically toggled on and off.  When the ring of strength row is removed, the "Linked" validation record is also removed, but everything else entered remains. Questions Can anyone think of a decent way to handle the variety of mathematical operations, dice rolls, and attribute references that would be easy and intuitive for a non-technical user to understand?  My current implementation is for them to be able to mouse-over a field and get a tooltip that says "@{attribute-name}" to reference the attribute in the Modifications section formulas.  For example, if you drink a potion that gives you an extra 2 hit points per hit dice, you would simply put in "@{hit-dice}*2" and set the operation value to 'modify'. However, this presents the risk of typos and could also be too complicated for users who struggle with these concepts. From a UX standpoint, what can be done to simplify the user's clicks to completion for this workflow? Gaining an item can require them to make changes to at least one repeating section, but as many as 3.  It also requires a new row for each attribute changed.  I was thinking about pre-building a number of validation rules that can be added when the sheet is created that I might expect most users to need. Are there any Roll20 limitations I have failed to consider in this implementation?
1576545154
The Aaron
Roll20 Production Team
API Scripter
That sounds like a massive undertaking.  I don't have enough experience with sheet workers to help you with that, but if you run into any specific problems, I'd be happy to be your sounding board on them. =D
1576600016

Edited 1576601112
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
So, this is kind of complex topic. In my opinion this system needs to be integrated into the sheet from line 1, rather than tacked on after the fact. I made a system that does the basic mods (and respects type stacking if that's needed) in the Starfinder HUD (later the Starfinder by Roll20) sheet. The handling of limited item slots can be handled pretty easily, and the two methods can be combined nicely as well. You can see how the basic mods handling works by looking at the starfinder sheet ( parseBuffs() , affectWhat() , applyBuffs() , and the massive listener that calls accessStarfinder() specifically). But the basic idea can be summed up as below: You'll have an object that contains all the data on what every attribute affects, what the formulas are for calculating them (e.g. (@{con_mod}+*{hit_die})*@{level}). In the SF sheet I called these attributeObjs for PC calculations, and npcAttributeObjs for npc calculations. This looks something like this (of course what keys you use and what not will depend on your sheet and system requirements): attributeObjs = { 'survival_buff': { attr: 'survival_buff', affects: ['survival'] }, //profession attributes "profession|profession": { "repeating": "profession", "attr": "profession", "affects": [], "isSkill": true, "numAdds": 0, "attrAdds": [], "repeatAdds": ["ranks", "class_skill"], "ability": "ability", "repeatTextAdds": [], "textAdds": [], "moddable": "_change", "rollTime": "misc" }, } You'll have a massive listener that will call a conductor function that determines what to do with the event. You can streamline this from how the SF sheet does it by using _.each to iterate through an array of attribute names and create a listener for each one. The conductor function might look at the event and decide it doesn't need to be reacted to, or that it needs some preprocessing, or that it just goes through the normal calculation. This function will also do the getAttrs() for the entire sheet. You'll be getting all the attributes you might possibly need in this one step, and then passing the created attribute object down the line. You'll also probably want to create a setObj object here that will also be passed down the line and will accumulate all the attribute changes that will be made.  You'll have a parseBuffs() function that pulls the values of all the buff fields. In the Starfinder sheet, I made these fields textareas and each modification that a given item/ability/buff provided was written on a new line. The parseBuffs() function will pull out the buff value, the buff type, and the target information for each activated (e.g. equipped or enabled) buff (e.g. what attribute does it affect, and are there any restricitions. Importantly, the parseBuffs() function should NOT replace attribute calls. The script then needs to figure out what attributes are being affected (and what order they need to be affected in). This is done in the affectWhat() and applyBuffs() functions on the SF sheet. Essentially, you'll take the information from the object I mentioned at the top and combine it with the buff values to add any attributes that a buff containing an attribute call is targeting to the affects  key of that attribute. As an example, a buff like +@{int_mod} to initiative  would add initiative to the affects  of the int_mod  attribute. Finally, you get to applyBuffs() , which takes all of this information and then expands attribute calls, formulas, and buff values to calculate the new value. At the end of applyBuffs() , you do a single setAttrs() that will apply all the accumulated changes. Adding handling for limited equipment slots should be relatively easy here. Just add a special function that is called whenever an item's equipped_location  attribute is changed that will check if that location is empty and variously allow the change, replace an already equipped item (in the case of something like armor that only has a single slot), or toggle a hidden checkbox for not enough slots. parseBuffs() would then need some extra logic to only pull buffs from equipped items that there are enough slots to accomodate. As for the UI issue, my preferred method of entering these on the SF sheet is to just type them in the textarea manually (but then again, I designed the syntax so I'd hope I was comfortable with it). I also added a mod wizard for users to enable that creates a pseudo-popup repeating section that separates each buff declaration into its own repeating item and has fields for the buff value (e.g. +2), the buff type (e.g. luck), and the target (e.g. hp). It works well for uncomplicated buffs, but is more cumbersome for complex things like "+2 luck bonus to the save DC of all abilities from the soldier class that use dexterity as the basis for their save". Hope that helps, and happy holidays, Scott EDIT: Some specific answers to your 3 questions at the bottom: Can anyone think of a decent way to handle the variety of mathematical operations, dice rolls, and attribute references that would be easy and intuitive for a non-technical user to understand? This is mostly handled by what I've outlined above, but I should point out that each moddable value that I expect to be sent to chat has an extra hidden _misc  field that stores the complete calculated buff values. Any buff declarations that can't be expanded by the sheetworker, either because it's a roll or calls an attribute the sheetworker doesn't have, is added here as it was written in the mods textarea and wrapped in a keep highest wrapper for each buff type if it doesn't stack. This allows buffs to contain rolls (e.g. +1d6), calls to custom attributes or attributes on another sheet, or even roll queries. Are there any Roll20 limitations I have failed to consider in this implementation? Not a Roll20 limitation so much as something I see a lot of sheet authors not thinking of. Create a defined attribute naming scheme, AND STICK TO IT. This will make creating functions to do many generic tasks a lot easier as you'll always know that the attribute containing what ability mod to apply to the skill is always going to be skill_name_ability_selection and the ranks in that skill will always be skill_name_ranks, and so on.
I kind of have a similar system.  I've got an attribute class that I use to register every attribute into an object on the first pass, then iterate over the objects keys to create the handler: class Attribute {     constructor(name,calculator,dependencies) {         this.name = name;         this.key = sanitize(name);         this.calculator = calculator; //(valuesObj,settterObj,bonusObj) => { } : setterObj     } } let getVal = (attribute,valuesObj,setterObj) => {     setterObj = setterObj || {};     return setterObj[attribute] || valuesObj[attribute]; }); let calculateBonuses = (valuesObj,setterObj) => {     let keys = _.union(Object.keys(valuesObj),Object.keys(setterObj));     let modificationGroupKeys = _.filter(keys,(key) => { // Gets a list of keys for all enabled modification groups         return key.indexOf('repeating_modification-group') === 0             && key.substr(key.length - 7) === 'enabled'             && valuesObj[key] == '1';     });     //A lot more code here that I'm still finishing up. }); registerAttribute(new Attribute('strength',(valuesObj, setterObj, bonusObj) => {     setterObj['strength'] = getVal('strength-roll',valuesObj,setterObj)         + getVal('strength-temp-bonus',valuesObj,setterObj)         + bonusObj['strength'];     return setterObj; }, ['strength-roll','strength-temp-bonus]); I also have a sorting function in place that ensures that no attribute is placed before it's dependencies in the stack.  The stack is then processed in order, with the results being saved to the setterObj, and that setterObj being passed into the next calculator. For the most part, everything is working as intended to calculate the sheet.  Right now I'm working on handling the modifiers and getting them to parse other repeating sections for their values (such as linking the 'Ring of Regeneration' bonus to regeneration to the 'Equipped' attribute for the repeating section for rings with the row Id for 'Ring of Regeneration'). As far as integrating with my character sheets, I'm building all of these components using handlebars and typescript and compiling them down to html and javascript based on the approprate context, so they're fitting in seamlessly with my other templates.  I'm able to generate my sheets for our homebrew 5e, D&D 3.5E, Heroes Unlimited, and an entirely homebrew system that a friend is working on all at once.  It's pretty great. :D