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

[Sheet Wokers HELP] Spoof a Change Event

1545478854
Finderski
Pro
Sheet Author
Compendium Curator
I'm re-writing the Savage Worlds Tabbed sheet from the ground up and will need to have an upgrade process.  Many of the existing sheet workers could handle the task, but I don't have them written as functions (and really would rather not try and do that).   Is it possible to make a sheet worker think something has changed so the on("change:..." triggers?
1545481971
GiGs
Pro
Sheet Author
API Scripter
Normally to trigger an upgrade, you create a new attribute, call it version, then create a function set to run on sheet:opened,  which checks if the sheet version attribute has been set. If it doesnt exist or is less than the new version, run whatever upgrade process you need, and at the end set the version attribute. That way when the sheet is opened again, the function will run, see the version exists and is the right value, so doesnt run the upgrade process again.
1545485511
Finderski
Pro
Sheet Author
Compendium Curator
GiGs said: Normally to trigger an upgrade, you create a new attribute, call it version, then create a function set to run on sheet:opened,  which checks if the sheet version attribute has been set. If it doesnt exist or is less than the new version, run whatever upgrade process you need, and at the end set the version attribute. That way when the sheet is opened again, the function will run, see the version exists and is the right value, so doesnt run the upgrade process again. Yep, that's what I'm talking about, though. I'd like that upgrade process to trigger a faux change to an attribute so the on("change:..." is hit. Otherwise, I'll need to create a separate function to handle it.  The on change stuff is already done (not in function format, and I'm hoping to avoid that, because more headache than I want).
1545486039
Wes
Sheet Author
Wouldn't this be a good use case for the  Sheet worker scripts Action Buttons  .
1545486259
Finderski
Pro
Sheet Author
Compendium Curator
Wes said: Wouldn't this be a good use case for the  Sheet worker scripts Action Buttons  . Not sure I follow...do you mean, make the upgrade process tied to the button?  Either way, I still don't want to have to convert all my on change things to functions. :(
1545487488
GiGs
Pro
Sheet Author
API Scripter
Finderski said: GiGs said: Normally to trigger an upgrade, you create a new attribute, call it version, then create a function set to run on sheet:opened,  which checks if the sheet version attribute has been set. If it doesnt exist or is less than the new version, run whatever upgrade process you need, and at the end set the version attribute. That way when the sheet is opened again, the function will run, see the version exists and is the right value, so doesnt run the upgrade process again. Yep, that's what I'm talking about, though. I'd like that upgrade process to trigger a faux change to an attribute so the on("change:..." is hit. Otherwise, I'll need to create a separate function to handle it.  The on change stuff is already done (not in function format, and I'm hoping to avoid that, because more headache than I want). I'm not really following you. Why do you need to trigger a faux change to an attribute? What would it need to do that isn't doable through the sheet:opened  version idea I suggested?
1545488327
Finderski
Pro
Sheet Author
Compendium Curator
I'd use the sheet:opened for the upgrade process.  However, I've converted a bunch of autocalc fields to sheet worker. I could use sheet opened for those,  too, but don't really see a need beyond the initial upgrade.  So, I either need to create a function for a one time use during the upgrade, or...I get the system to think there was a change event on one of the fields. It's more than just autocalc to sheet worker things, though. I have added a few new fields to things like Strength, etc. These are fields that are hidden and changed based on an update to the attribute itself. So, I don't need a whole function to handle the on change stuff, because that is essentially the function. A change in strength happens, on change catches and makes the necessary updates.  For existing characters (the ones needing the upgrade), those fields aren't likely going need to be updated immediately, but other portions of the sheet (like roll buttons) will need those additional fields. So, I either need to create a function to create ALL those additional fields or, I just fake a change to each of the attributes and let the on change handle it. Does that make sense? Essentially, this is what I'm wanting to do is something like... const upgradefunction() { //do upgrade stuff trigger faux change to attribute //let the existing on change stuff hanlde it } sheet:opened ({ if version != current upgradefunction set version = current else done }) If I can't trigger a fake event (and I fear I can't, but just hoping someone has an idea), then I'll need more functions to create those fields that don't exist on the current sheet...or rather to create those additional fields AND make sure they have the correct value.
1545489792

Edited 1545489994
GiGs
Pro
Sheet Author
API Scripter
You cant trigger a fake event. You could trigger two events (like adding +1 to a stat and then subtracting 1 from it), but with the async nature of sheets, this is fraught with peril.  There is a way to do it, i think, but its probably more work than just handling it in the upgrade function. If I understand correctly, you have a bunch of dependent attributes you want to be created, and they are based on some master attributes. So, you could  a) add sheet:opened  to the master attribute's sheet worker, so it runs every time the sheet opens, even if no attribute changed b) the worker includes a check that the dependent attributes have the value they should have, and if not, sets them. Depending on how your sheet works, this could be a very natural and simple change, or it could be a very messy and convoluted one. Personally, I think it's always safest to handle these changes directly within the upgrade function. It's the best time to validate the sheet, and the function is built to fire only once, so you have full control and dont need to worry about it firing extra times, in unpredictable ways. If you're worried about the upgrade function being too big, you can probably use arrays and loops to handle all the attributes you need to change in a fairly compact function (especially if there's a consistent pattern to your linked attribute names).  Is that the reason you're looking for an alternative solution to handling everything with the upgrade function?
1545490188
Finderski
Pro
Sheet Author
Compendium Curator
Partially...but mostly to save me the hassle of trying to figure out the code necessary for a function.  My JS skills are so weak it takes forever to get a sheet worker to function properly...like FOREVER.  So, as an example of what I don't want to do again... //Create Trait values for rank and display hidden fields on("change:agility change:agMod change:smarts change:smMod change:spirit change:spMod change:strength change:stMod change:vigor change:viMod change:fighting change:fightingmod change:academicsskill change:academicsskillmod change:athletics change:athleticsmod change:battle change:battlemod change:boating change:boatingmod change:climbing change:climbingmod change:commonknowledge change:commonknowledgemod change:driving change:drivingmod change:electronics change:electronicsmod change:faith change:faithmod change:focus change:focusmod change:gambling change:gamblingmod change:guts change:gutsmod change:hacking change:hackingmod change:healing change:healingmod change:intimidation change:intimidationmod change:investigation change:investigationmod change:language change:languagemod change:lockpicking change:lockpickingmod change:notice change:noticemod change:occult change:occultmod change:performance change:performancemod change:persuasion change:persuasionmod change:piloting change:pilotingmod change:psionics change:psionicsmod change:repair change:repairmod change:research change:researchmod change:riding change:ridingmod change:ritual change:ritualmod change:science change:sciencemod change:shooting change:shootingmod change:spellcasting change:spellcastingmod change:stealth change:stealthmod change:streetwise change:streetwisemod change:survival change:survivalmod change:swimming change:swimmingmod change:taunt change:tauntmod change:throwing change:throwingmod change:thievery change:thieverymod change:tracking change:trackingmod change:weirdscience change:weirdsciencemod change:repeating_skills:skillnamerank change:repeating_skills:skillnamemod",function(eventInfo) {     console.log("^^^^ eventInfo details: "+eventInfo.sourceAttribute);     var checkString;     console.log("Slice of eventInfo: "+eventInfo.sourceAttribute.slice(0,16));     if (eventInfo.sourceAttribute.slice(0,16) === "repeating_skills") {         console.log("*** Hit a Repeating Section ***");         checkString = eventInfo.sourceAttribute.slice(-13);         console.log("*** checkString: "+checkString+" ***");     }     else { checkString = eventInfo.sourceAttribute; }     switch(checkString) {        case 'agility':        case 'agmod':            var cTrait = "agility", cMod = "agMod", sAttRank = 'agility_rank', sAttDisplay = 'agility_display';            var chTrait = ["agility","agMod"];            break;        case 'smarts':        case 'smmod':            var cTrait = "smarts", cMod = "smMod", sAttRank = 'smarts_rank', sAttDisplay = 'smarts_display';            var chTrait = ["smarts","smMod"];            break;        case 'spirit':        case 'spmod':            var cTrait = "spirit", cMod = "spMod", sAttRank = 'spirit_rank', sAttDisplay = 'spirit_display';            var chTrait = ["spirit","spMod"];            break;        case 'strength':        case 'stmod':            var cTrait = "strength", cMod = "stMod", sAttRank = 'strength_rank', sAttDisplay = 'strength_display';            var chTrait = ["strength","stMod"];            break;        case 'vigor':        case 'vimod':            var cTrait = "vigor", cMod = "viMod", sAttRank = 'vigor_rank', sAttDisplay = 'vigor_display';            var chTrait = ["vigor","viMod"];            break;        case 'fighting':        case 'fightingmod':            var cTrait = "fighting", cMod = "FightingMod", sAttRank = 'fighting_rank', sAttDisplay = 'fighting_display';            var chTrait = ["fighting","FightingMod"];            break;        case 'boating':        case 'boatingmod':            var cTrait = "boating", cMod = "BoatingMod", sAttRank = 'boating_rank', sAttDisplay = 'boating_display';            var chTrait = ["boating","BoatingMod"];            break;        case 'skillnamerank':        case '_skillnamemod':            var cTrait = "repeating_skills_SkillNameRank", cMod = "repeating_skills_SkillNameMod", sAttRank = 'repeating_skills_otherskill-rank', sAttDisplay = 'repeating_skills_otherskill-display';            var chTrait = ["repeating_skills_SkillNameRank","repeating_skills_SkillNameMod"];            break;        default:            console.log("Changed Attribute: "+eventInfo.sourceAttribute);            var firstLetter = eventInfo.sourceAttribute.slice(0,1);            if (eventInfo.sourceAttribute.slice(-3) === "mod") {                //a mod was changed                var cTrait = eventInfo.sourceAttribute.slice(0,eventInfo.sourceAttribute.length - 3);                var cMod = firstLetter.toUpperCase() + eventInfo.sourceAttribute.slice(1);                cMod = cMod.replace("mod","Mod");            }            else {                var cTrait = eventInfo.sourceAttribute;                var cMod = firstLetter.toUpperCase() + eventInfo.sourceAttribute.slice(1)+"Mod";            }            var sAttRank = cTrait + '_rank', sAttDisplay = cTrait + '_display';            var chTrait = [cTrait, cMod];            console.log("Fields to be updated: "+cTrait+", "+cMod);            break;     }          console.log("Attributes to set: "+sAttRank+", and "+sAttDisplay); console.log("Attributes to get Info from: "+cTrait+", and "+cMod);     getAttrs(chTrait, function(v) {         console.log(cTrait+" value: "+v[cTrait]);         console.log(cMod+" value: "+v[cMod]);         var tRank, tDesc;         tRank = '1d'+v[cTrait]+'!';         console.log("Rank set to: "+tRank);         tDesc = 'd'+v[cTrait];         console.log("Display set to: "+tDesc);         var intMod = parseInt(v[cMod])||0;         if (intMod != 0) {             tRank += '+'+intMod+'['+cTrait+']';             tDesc += '+'+intMod;         }         const sattrs = {};         sattrs[sAttRank]=tRank;         sattrs[sAttDisplay]=tDesc;         console.log("##### sattrs: " + JSON.stringify(sattrs) + " #####");         setAttrs(sattrs);     }); });
1545492546
GiGs
Pro
Sheet Author
API Scripter
I'm about to go to bed, I'll look at that after I sleep. From skimming it though, it does look like it might be a bit more complicated than it needs to be, and I may have streamlining suggestions once i look closer. Is this function involved in your upgrade issues, and how?
1545537913

Edited 1545538602
GiGs
Pro
Sheet Author
API Scripter
coincidentally, I have been planning a post on how to write sheetworkers that apply to multiple stats like this, so this is a bit of practice for it :) Here's the basics of how I would rewrite the above script. For this first draft, there are some stats that dont work, I'll explain afterwards and mention how to fix it for those too. First you create an array of the stats needed like this: const traits = ['agility', 'smarts', 'spirit', 'strength', 'vigor', 'fighting', 'academicsskill', 'athletics',  'battle', 'boating', 'climbing',  'commonknowledge', 'driving', 'electronics', 'faith', 'focus', 'gambling',  'guts', 'hacking', 'healing', 'intimidation', 'investigation', 'language', 'lockpicking', 'notice', 'occult',  'performance', 'persuasion', 'piloting', 'psionics', 'repair', 'research', 'riding', 'ritual', 'science',  'shooting', 'spellcasting', 'stealth', 'streetwise', 'survival', 'swimming', 'taunt', 'throwing', 'thievery',  'tracking', 'weirdscience', 'repeating_skills:skillnamerank', ]; You'll notice a bunch of traits are missing from that list - all the ones with 'mod' in their name. Then you follow that immediately with this sheet worker: traits.forEach(trait => { on(`change:${trait} change:${trait}mod`,function(eventInfo) { getAttrs([trait,`${trait}Mod`], v => { let tRank = '1d'+v[trait]+'!'; let tDesc = 'd' + v[trait]; let intMod = parseInt(v[`${trait}_mod`])||0; if(intMod !== 0) { tRank += `+${intMod}[trait]`; tDesc += '+' + intMod; } const sAttRank = trait + '_rank', sAttDisplay = trait + '_display'; const sattrs = []; sattrs[sAttRank]=tRank; sattrs[sAttDisplay]=tDesc; setAttrs(sattrs); )}; }); )}; I think I've decoded what your script does, and if so, the above should do it. It's much more compact and dare i say, elegant, approach than the original code. Thanks to the forEach loop, this creates a unique sheet worker for each trait and trait mod pair, so when the worker runs, you always know which trait is being called, and dont need eventInfo. Before I get to the cases the above doesnt cover, let me first mention a bit of syntax: `change:${trait} change:${trait}mod` On this line I used a string literal. This line is exactly the same as writing "change:" + trait + " change:" + trait + "mod" Once you're used to it though, the first version (string literal aka template literal) is a lot easier to handle when you writing strings that contain multiple variables. But use whichever you're most comfortable with. This overall approach is easy to maintain: if you later add more attributes, you can just add their names to the array at the start and don't need to edit the code at all. The method is also easily transferrable to other sheet projects.  It's a great approach to handling this kind of situation where you have one rule that applies to lots of different attributes. So on to the attributes the above doesnt quite work with and why. First point: I notice some inconsistency in the "Mod" part of names - some are listed as Mod in the change line, and they should always be lower case there. I am assuming for this script that the Mod in attribute names i always capitalised. The big issue: For most of your attributes you use a consistent "trait" and "traitmod" naming scheme. For the core stats (agility, smarts, etc) and the repeating set, you don't do that. Which means they aren't as simple to handle. The two best ways to ways to address this, in order: since you're doing a version update script, take the opportunity to rename the affected attributes so they do match the pattern ([trait], [trait]+mod) and transfer all the affected existing attributes to the new ones. It's more work initially, but i encourage you to do it. Alternatively, add in a little special cases check at the start of the above script so that it does work for them.  Here's what the special cases version would look like: traits.forEach(trait => { const specialcases = ['agility', 'smarts', 'spirit', 'strength', 'vigor', 'repeating_skills:skillname', ] let mod = trait + 'Mod', display = trait + "_display, rank = "_rank"; if(specialcases.includes(trait) ) { if (trait.slice(0,16) === 'repeating_skills') { mod = repeating_skills_SkillNameMod; rank = 'repeating_skills_otherskill-rank' display = 'repeating_skills_otherskill-display' } else { mod = trait.slice(0,2) + 'Mod'; } } on(`change:${trait.toLowerCase()} change:${mod.toLowerCase()}`,function() { getAttrs([trait, mod], function(v) { let tRank = '1d'+ v[trait]+'!'; let tDesc = 'd' + v[trait]; let intMod = parseInt(v[mod)]||0); if(intMod !== 0) { tRank += `+${intMod}[${trait}]`; tDesc += '+' + intMod; } const sattrs = []; sattrs[rank]=tRank; sattrs[display]=tDesc; setAttrs(sattrs); )}; }); )}; The specialcases array and the following if statement create the mod stat you need. I should stress I havent tested the above, so I cant rule out the possibilities of minor typos and syntax errors, but the principle is sound!  One question: does intMod ever go negative? If you you'd need to alter one section of the script (both mine and yours):                         if(intMod !== 0) { tRank += `+${intMod}[${trait}]`; tDesc += '+' + intMod; } should be something like the following to avoid getting situations like "+-"  if(intMod > 0) { tRank += `+${intMod}[${trait}]`; tDesc += '+' + intMod; } else if(intMod < 0) { tRank += `${intMod}[${trait}]`; tDesc += intMod; } or (if you are familiar with the ternary operator): if(intMod !== 0) { tRank += (intMod > 0 ? '+' : '') + `${intMod}[${trait}]`; tDesc += (intMod > 0 ? '+' : '') + intMod; }  I hope you find all this useful!
1545560792
Finderski
Pro
Sheet Author
Compendium Curator
GiGs said: coincidentally, I have been planning a post on how to write sheetworkers that apply to multiple stats like this, so this is a bit of practice for it :) Here's the basics of how I would rewrite the above script. For this first draft, there are some stats that dont work, I'll explain afterwards and mention how to fix it for those too. First you create an array of the stats needed like this: const traits = ['agility', 'smarts', 'spirit', 'strength', 'vigor', 'fighting', 'academicsskill', 'athletics',  'battle', 'boating', 'climbing',  'commonknowledge', 'driving', 'electronics', 'faith', 'focus', 'gambling',  'guts', 'hacking', 'healing', 'intimidation', 'investigation', 'language', 'lockpicking', 'notice', 'occult',  'performance', 'persuasion', 'piloting', 'psionics', 'repair', 'research', 'riding', 'ritual', 'science',  'shooting', 'spellcasting', 'stealth', 'streetwise', 'survival', 'swimming', 'taunt', 'throwing', 'thievery',  'tracking', 'weirdscience', 'repeating_skills:skillnamerank', ]; You'll notice a bunch of traits are missing from that list - all the ones with 'mod' in their name. Then you follow that immediately with this sheet worker: traits.forEach(trait => { on(`change:${trait} change:${trait}mod`,function(eventInfo) { getAttrs([trait,`${trait}Mod`], v => { let tRank = '1d'+v[trait]+'!'; let tDesc = 'd' + v[trait]; let intMod = parseInt(v[`${trait}_mod`])||0; if(intMod !== 0) { tRank += `+${intMod}[trait]`; tDesc += '+' + intMod; } const sAttRank = trait + '_rank', sAttDisplay = trait + '_display'; const sattrs = []; sattrs[sAttRank]=tRank; sattrs[sAttDisplay]=tDesc; setAttrs(sattrs); )}; }); )}; I think I've decoded what your script does, and if so, the above should do it. It's much more compact and dare i say, elegant, approach than the original code. It's definitely more elegant. :) And thanks for this, my plan was to try and figure out the forEach thing and use.  Quick question...with a forEach, does that mean it will do that for every trait, every time one of them changes? I'm guessing it does NOT, given you statement immediately following, but...really that could interpreted 2 ways: we know because the loop always deals with one at a time, so we don't need eventInfo, OR we know because only one is executed based on what changed. Just wanting to make sure I understand it. GiGs  said: <snip> So on to the attributes the above doesnt quite work with and why. First point: I notice some inconsistency in the "Mod" part of names - some are listed as Mod in the change line, and they should always be lower case there. I am assuming for this script that the Mod in attribute names i always capitalised. The big issue: For most of your attributes you use a consistent "trait" and "traitmod" naming scheme. For the core stats (agility, smarts, etc) and the repeating set, you don't do that. Which means they aren't as simple to handle. Yeah...this sheet was started before sheet workers AND before I really knew what I was doing. The danger of players losing data is what has stopped me from changing them as I re-write the sheet. I probably makes sense to change them...it just means a lot more testing before deployment, because not getting paid and spending A LOT of time on something, and then having people get upset because some data was lost really makes it difficult to want to keep doing it...but ultimately, you are right with your suggestion to take the time to do it right. GiGs  said: I hope you find all this useful! Extremely! Thanks again.
1545561549

Edited 1545561660
GiGs
Pro
Sheet Author
API Scripter
 Quick question...with a forEach, does that mean it will do that for every trait, every time one of them changes? I'm guessing it does NOT,  You're correct it doesn't.  Essentially, when the sheet is loaded, the forEach script runs, and creates all the sheetworkers. They each then exist independently, as if you'd manually created all of them as separate workers. They then exist, waiting passively for their triggers, and each responds only to their unique triggers.  It was a bit mindblowing for me when I discovered you could create events in this way. It really is powerful and flexible.
1545562039
Finderski
Pro
Sheet Author
Compendium Curator
Awesome. Then, one last question...since I need to do this for the upgrade script and I can't kick off a "fake change," can I create a separate function that the on change stuff calls? Something like: const setRankDisplay = function(trait, mod) { getAttrs([trait,`${trait}Mod`], v => { let tRank = '1d'+v[trait]+'!'; let tDesc = 'd' + v[trait]; let intMod = parseInt(v[`${trait}_mod`])||0; if(intMod !== 0) { tRank += `+${intMod}[trait]`; tDesc += '+' + intMod; } const sAttRank = trait + '_rank', sAttDisplay = trait + '_display'; const sattrs = []; sattrs[sAttRank]=tRank; sattrs[sAttDisplay]=tDesc; setAttrs(sattrs); )}; } const traits =[<list of traits>]; traits.forEach(trait => { on(`change:${trait} change:${trait}mod`,function(eventInfo) { setRankDisplay(`${trait}, ${trait}mod`); }); }); That way, the upgrade script could call the function directly?
1545564094

Edited 1545564111
GiGs
Pro
Sheet Author
API Scripter
That's a good question. I think that should work, but it's possible the async nature of roll20 might be a stumbling block. It's easy to test by rewriting one of your simplest sheetworkers. Whether it works or not has nothing to do with the forEach or passing of variables, its just checking if the function call works. So a simple function with a single change event, in this format:         on("change:(whatever stat)",function(eventInfo) { setRankDisplay(); }); and move the contents of that simple sheet worker out into a function like the above and see if it still works.
1545568607
GiGs
Pro
Sheet Author
API Scripter
I just created this simple worker to change the update the value of one attribute with another, on my test sheet, and it worked fine: const setRankDisplay = function(label) {    getAttrs(["starting"], function(v) {             let starting = parseInt(v["starting"])||0;     setAttrs({ final: starting });     }); }; on("change:starting sheet:opened", function() {     setRankDisplay("testing"); });