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

Sheetworker hides attribute

1657240200

Edited 1657240480
vÍnce
Pro
Sheet Author
I've created a sheetworker that uses the dexterity attribute to determine a dual-wield penalty (primary vs secondary attack) within repeating_weapon.  The worker seems to function as expected but for some reason an API script(scriptCards but might also be an issue for the API in general...) doesn't always have access to the calculated attribute "weapon_dual_pen".  For example; after the repeating selector is used to set "primary" or "secondary", weapon_dual_pen value is detected, but if I then change Dex which forces a new calculation for weapon_dual_pen, it now vanishes from the API.  The attack still works fine and the calcs are correct, but it's no longer detectable by scriptCards unless I re-toggle the repeating selector or tab in/out of weapon_dual_pen field.  Apologies in advance for my hack-n-slash js and/or any obvious mistakes. ;-) TIA Thoughts? html <span class="label-under" title="Dual-wield attack? Defaults to Primary -2, Secondary -4 and then auto-calculated according to Dex."> <span style="display:inline-flex;"> <select class="width-small-plus" name="attr_weapon_dual" title="@{repeating_weapon_$X_weapon_dual}" style="margin: 1px 0 6px 0;height: 1.5em;padding: 0;"> <option value="" selected>Normal</option> <option value="Primary">Primary</option> <option value="Secondary">Secondary</option> </select> <input type="text" class="width-smaller" name="attr_weapon_dual_pen" title="@{repeating_weapon_$X_weapon_dual_pen}" value="0" /> </span> <label>Dual-Wield</label> </span> worker on('change:repeating_weapon:weapon_dual change:dexterity', (eventInfo) => { clog(`Change Detected:${eventInfo.sourceAttribute}`); getSectionIDs('repeating_weapon', (idArray) => { const fieldnames = []; idArray.forEach((id) => { fieldnames.push(`repeating_weapon_${id}_weapon_dual`); }); getAttrs(['dexterity', ...fieldnames], (v) => { clog('Weapon Attack Type has been re-calculated'); const output = {}; idArray.forEach((id) => { const dex = +v.dexterity || 0; const thisflagStr = v[`repeating_weapon_${id}_weapon_dual`]; let dexMod = 0; let thisflag = 0; if (thisflagStr !== '') { thisflag = +0; if (dex < 6) { if (dex === 5) { dexMod = -1; } if (dex === 4) { dexMod = -2; } if (dex <= 3) { dexMod = -3; } clog(`low dex penalty: ${dexMod}`); } if (dex > 15) { if (dex === 16) { dexMod = 1; } if (dex === 17) { dexMod = 2; } if (dex >= 18 && dex <= 20) { dexMod = 3; } if (dex >= 21 && dex <= 23) { dexMod = 4; } if (dex >= 24) { dexMod = 5; } clog(`high dex penalty: ${dexMod}`); } } if (thisflagStr === 'Primary') { thisflag = +-2; } if (thisflagStr === 'Secondary') { thisflag = +-4; } // do not give a bonus for high Dex, so cap at "0" const thispenalty = Math.min(thisflag + dexMod, 0); output[`repeating_weapon_${id}_weapon_dual_pen`] = thisflag === 0 ? '0' : thispenalty; }); setAttrs(output, { silent: true, }); }); }); }); scriptCards macro to determine what repeating attributes are detected !script {{ --Rfirst|@{selected|character_id};repeating_weapon --Rdump| }}
1657249372
GiGs
Pro
Sheet Author
API Scripter
My guess it's something to do with the way scriptcards interacts with repeating sections. i'd recommend raising this question in the scriptcards thread. Btw, your if statements could be made a lot simpler if you embrace the use of else in your if statements. Also, do you need the high dex calculations? If I'm following the code correctly, won't they always be discarded when you set the max to 0?
1657265115
John D.
Pro
Sheet Author
It looks like the high dex applies when not dual wielding, no? I was thinking the same thing about not reliably reading repeating rows.  Also was envisioning using either a switch statement instead of ifs, or an object where dex ranks are the keys and mods the values.  Vince, have you tried using this script to set an attribute outside of a repeating row and reading it from API, or even outside of the scriptCard code? 
1657267378
GiGs
Pro
Sheet Author
API Scripter
John D. said: It looks like the high dex applies when not dual wielding, no? You made me look again, and it looks like the dex mod only applies during dual wielding. But those high dex mods reduce the penalty. That said, low dex only penalises while duel wielding - i could see that making sense: don't duel wield if you have low dex, it makes the penalty worse. John D. said: Vince, have you tried using this script to set an attribute outside of a repeating row and reading it from API, or even outside of the scriptCard code?  That's a great suggestion.
1657329708
vÍnce
Pro
Sheet Author
Great suggestions. I knew there must be a better method for handling the calculation but alas I used my primate-level js skills at best (apologies to all primates) and was able to make it work, albeit less than optimal, and moved on to my next sheetworker conundrum...  I will try my hand at re-writing.  Switch or an object json? (not sure how this works, must investigate). Moving the calc to a non-repeating attribute is definitely on the table.  I'm hoping that will solve the hide and seek issue I'm seeing with scriptcards. High Dex: maybe I'm looking at this wrong but I think I have to extrapolate up the max bonus of "5" because the "0" cap should only matter for a secondary attack (-4) if someone has >=24 right?  Or >=17 for a secondary attack (-2).
1657331867

Edited 1657333311
GiGs
Pro
Sheet Author
API Scripter
vÍnce said: Switch or an object json? Either of those could be done, but I was thinking an approach using if (and else ) could be a lot less clunky. Just to confirm: does the dex modifier apply only to reduce the two weapon penalty, and doesn't apply on normal attacks?
1657335454

Edited 1657335511
vÍnce
Pro
Sheet Author
GiGs said: vÍnce said: Switch or an object json? Either of those could be done, but I was thinking an approach using if (and else ) could be a lot less clunky. Just to confirm: does the dex modifier apply only to reduce the two weapon penalty, and doesn't apply on normal attacks? In regards to dual-wielding; the Dex mod only applies to a primary and secondary dual-wieling melee attack. A normal melee attack does not get to apply a dex mod. (Ad&d 1e)
1657345484

Edited 1657397512
GiGs
Pro
Sheet Author
API Scripter
Before I give a sample streamlined way to write the code, I'm going to explain my reasoning. The first thing to understand about if statements in javascript: JS stops working through the code the instant it finds a true result. To explain what this means. Your if statement is structured like this:              if (dex === 5) { } if (dex === 4) { } if (dex <= 3) { } We aren't concerned about what's inside each code block, just the overall structure. In the above code, that is three completely separate if statements. lets say that dex is in fact 5. It will look in the if statement, find a matching result, run that code. And then it will go to the next if statement, and test if dex is 4. And then it will go to the next statement, and check if it is below or equal to 3. Now look at this code: if (dex === 5) { } else if (dex === 4) { } else if (dex <= 3) { } This is in fact one if statement. If dex is 5, it will look at the first one, find a match, then completely skip over the rest. There's another benefit to doing it this way. Your other dex tests are like this: if (dex === 16) { } if (dex === 17) { } if (dex >= 18 && dex <= 20) { } if (dex >= 21 && dex <= 23) { } if (dex >= 24) { } There's some complex logic in there like if (dex >= 21 && dex <= 23), that you don't need if you use else, and restructure the code. For instance, you could do this: if (dex >= 24) { } else if (dex >= 21) { } else if (dex >= 18) { } else if (dex === 17) { } else if (dex === 16) { } Remember that the if statement stops when it finds a matchign result, and skips the rest. So you can use if (dex >= 21) - if the if statement reaches that test, it must be below 24, because it didnt stop at the first test. So you don't need the complex logic. When building if statements like this, you do have to be careful to build the tests in the correct order. That's why I reversed the order - you need to test for 24+ before 21+ and before 18+. Those are the benefits of using else. There's another thing to do: analyse the actual numbers. Let's look at the first steps: if (dex >= 24) {                 dexMod = 5; } else if (dex >= 21) {                dexMod = 4; } else if (dex >= 18) {                 dexMod = 3 } Here, we can see every 3 points of dex gives a +1 bonus up to a max of 5. That is the same as Math.floor(dex / 3) - 3. If we want to limit it to a max of 5, we can use Math.min(). So we can write that as if (dex >= 18) {                 dexMod = Math.min(5, Math.floor(dex / 3) - 3); } else if (dex > 15) { dexMod = dex-15; } In the above statement we take advantage that dex 16 gives +1, and dex 17 gives +2. In other words, we don't need to include every single dex step: we look at what patterns the system uses, and identify rules for them. Your complete dex statement would then look like this: if ( dex < 6 ) {     dexMod = Math . max (- 3 , dex - 6 );     clog ( `low dex penalty: ${ dexMod } ` ); } else if ( dex > 15 ) {     if ( dex >= 18 ) {         dexMod = Math . min ( 5 , Math . floor ( dex / 3 ) - 3 );     } else {         dexMod = dex - 15 ;     }     clog ( `high dex penalty: ${ dexMod } ` ); } Notice there are a few tweaks here. First the dex scores below 6 all follow the same rule: modifier is [dex -6] (5 = -1, 4 - 2, 3 - 3), to a max of -3. Second there are only two rules for dex scores above 15, and since the branch already checks if dex > 15, you don't need to check for that again - a simple else suffices since everything inside that branch must already by > 15. Honestly that second branch having a nested if statement looks ungainly to me. I might write it more like this: if ( dex < 6 ) {     dexMod = Math . max (- 3 , dex - 6 );     clog ( `low dex penalty: ${ dexMod } ` ); } else if ( dex >= 18 ) {     dexMod = Math . min ( 5 , Math . floor ( dex / 3 ) - 3 );     clog ( `very high dex penalty: ${ dexMod } ` ); } else if ( dex > 15 ) { dexMod = dex - 15 ;     clog ( `high dex penalty: ${ dexMod } ` ); } It's the use of else that allows streamlined if statements like this. That last one is dex >15, but dex 18 never reaches it because the if statement stops at dex >=18. If you didn't need the clog function calls there, I'd go further and use a ternary operator like: const dex_mod = ( dex < 6 ) ? Math . max (- 3 , dex - 6 ) :     ( dex >= 18 ) ? Math . min ( 5 , Math . floor ( dex / 3 ) - 3 ) :     ( dex > 15 ) ? ( dex - 15 ) : 0 ; That way you don't need a separate let dex_mod = 0; initialisation. Though that's not as easily readable, especially if you aren't very comfortable with ternary operators. Now, another inefficiency in the code is the dexMod calculation is inside the forEach loop. Once the sheet worker has started, dex doesn't change, so that really should be outside of the loop. You only need to calculate it once, not for every row of the section. If you want some output in each row, you could put a command in the forEach loop for that, but the dex calculation itself should be done at the start. Then you have this structure: if (thisflagStr !== '') { /* some stuff */             } if (thisflagStr === 'Primary') {             thisflag = +-2; } if (thisflagStr === 'Secondary') {             thisflag = +-4; } You already know how to use ternary operators - you have one in your worker -  so you can rewrite those three if statements as a single statement like so: if (thisflagStr !== '') {             thisflag = (thisflagStr === 'Primary') ? -2 : -4; /* some stuff */ } This is because thisflagStr can only have three results: '', 'Primary', and 'Secondary'. So to get inside that if code block, it must be either primary or secondary. When we check for primary, the other one must be the secondary result. Here's a javascript tip, to make that look neater: if (thisflagStr.length) {             thisflag = (thisflagStr === 'Primary') ? -2 : -4; /* some stuff */ } This checks how long thisflagStr is, and returns true for any result longer than 0. This works because in javascript, if statements look for truthy or falsy values - these are special javascript properties. Falsy is anything that calculates to false , zero , an empty string (''), null , or undefined . Everything else is truthy. So thisflagStr is truthy if the string contains anythign at all, but if its empty, its falsy. So in our case, where three entries are possible, the if resolves as truthy only if thisflagStr is primary or secondary. So, overall, I'd rewrite the worker something like this (there's one line my tendancy towards compactifying might have gone too far...): on ( 'change:repeating_weapon:weapon_dual change:dexterity' , ( eventInfo ) => {     clog ( `Change Detected: ${ eventInfo . sourceAttribute } ` );     getSectionIDs ( 'repeating_weapon' , ( idArray ) => {         const fieldnames = idArray . reduce (( fields , id ) => [... fields , `repeating_weapon_ ${ id } _weapon_dual` ], []);         getAttrs ([ 'dexterity' , ... fieldnames ], ( v ) => {             clog ( 'Weapon Attack Type has been re-calculated' );             const output = {};             const dex = + v . dexterity || 0 ;             // dex doesn't change within the loop, so should calculate it first.             const dex_mod = ( dex < 6 ) ? Math . max (- 3 , dex - 6 ) :                 ( dex >= 18 ) ? Math . min ( 5 , Math . floor ( dex / 3 ) - 3 ) :                 ( dex > 15 ) ? ( dex - 15 ) : 0 ;                         if ( dex_mod ) {                 clog ( ` ${ dex_mod > 0 ? 'high' : 'low' } dex penalty: ${ dex_mod } ` )             }             // a function to calculate the total bonus. This could be inside the loop, it just makes things easier to read like this (IMO).             const handed_mod = ( attack_type , dex_mod ) => attack_type . length ? dex_mod + ( attack_type === 'Primary' ? - 2 : - 4 ) : 0 ;                         idArray . forEach (( id ) => {                 const attack_type = v [ `repeating_weapon_ ${ id } _weapon_dual` ];                 const thispenalty = Math . min ( 0 , handed_mod ( attack_type , dex_mod ));                 output [ `repeating_weapon_ ${ id } _weapon_dual_pen` ] = thispenalty ;             });             setAttrs ( output , {                 silent : true             });         });     }); }); Feel free to ask questions :)
1657351270
vÍnce
Pro
Sheet Author
I'm always blown away by your coding efficiency and great explanations GiGs. I can see how using "else if" is a much better approach then testing EACH block of Dex values. And then you simplified that logic down to a single line(although it does look like there's a lot going on here); const dex_mod = dex < 6 ? Math.max(-3, dex - 6) : dex >= 18 ? Math.min(5, Math.floor(dex / 3) - 3) : dex > 15 ? dex - 15 : 0; I look at this line as multiple IF(true/false) tests. I didn't know you could include multiple ternary operators like that. :-) Just curious, will this test all the ternary cases or will it stop and return a value for the first truthy state? Truthfully I'm not 50% sure on how to properly use getSectionIDs and forEach.  I understand that you need getSectionIDs to interact with repeating attributes and I believe forEach loops through each repeating attribute, or row...?  Not sure.  ;-(  So I understand the benefit of moving a calc outside a loop, but what IS the loop actually? Feel free to use me as a case study GiGs. lol Your efforts very much appreciated!  Truly above and beyond.
1657391523

Edited 1657391624
GiGs
Pro
Sheet Author
API Scripter
vÍnce said: I'm always blown away by your coding efficiency and great explanations GiGs. I can see how using "else if" is a much better approach then testing EACH block of Dex values. And then you simplified that logic down to a single line(although it does look like there's a lot going on here); const dex_mod = dex < 6 ? Math.max(-3, dex - 6) : dex >= 18 ? Math.min(5, Math.floor(dex / 3) - 3) : dex > 15 ? dex - 15 : 0; I look at this line as multiple IF(true/false) tests. I didn't know you could include multiple ternary operators like that. :-) Just curious, will this test all the ternary cases or will it stop and return a value for the first truthy state? Thanks, Vince :) Some people would argue that "simplified" is the wrong word there, hehe. It's perfectly fine to prefer using let and conventional if statements especially if the code is shared - massive nested ternary operators like that aren't very readable for most people. (That's why I used line breaks and tab nesting to make it more readable, though a single line like you posted is exactly the same.) A nested ternary operator like this is just like a single if else statement - it will stop at the first truthy value. You can put a new ternary after each : (or even between a ? and :), and its common to surround each branch in brackets, like this: const dex_mod = dex < 6 ? Math.max(-3, dex - 6) : (dex >= 18 ? Math.min(5, Math.floor(dex / 3) - 3) : (dex > 15 ? dex - 15 : 0)); But that's not usually necessary, and makes it less readable to me - especially if you plan to break it over multiple lines for readability like I did I'll post about forEach and getSectionIDs in the next comment.
1657392201

Edited 1657397544
GiGs
Pro
Sheet Author
API Scripter
Looking at my worker I just noticed there's some duplicated code in these two lines: const handed_mod = (attack_type, dex_mod) => attack_type.length ? Math.min(0, dex_mod + (attack_type === 'Primary' ? -2 : -4)) : 0; const thispenalty = Math.min(0, handed_mod(attack_type, dex_mod)); Math.min(0,) is there in both lines, but it only needs to be in one of them. So, while you don't need to, I'd recommending changing either of the lines like so (don't do both - you need the Math.min statement on one of those lines, it just doesn't matter which): const handed_mod = (attack_type, dex_mod) => attack_type.length ? dex_mod + (attack_type === 'Primary' ? -2 : -4) : 0; const thispenalty = handed_mod(attack_type, dex_mod); (Edit: I changed the original code to account for this). See also there's an example of nesting a ternary operator inside the ? and :.
1657395378

Edited 1657397657
GiGs
Pro
Sheet Author
API Scripter
Is your confusion about forEach and getSectionIDs purely about getSectionIDs, or would you like an general explainer on the concept of loops like forEach as well?
1657397684
vÍnce
Pro
Sheet Author
GiGs said: Is your confusion about forEach and getSectionIDs purely about getSectionIDs, or would you like an genera; explainer on the concept of loops like forEach as well? Yes and yes. :-)
1657438754

Edited 1657438772
GiGs
Pro
Sheet Author
API Scripter
I havent had much time today. But lets see what i can cover quickly. First, think about idArray in getSectionIDs - this is an array of row ids. You have one row id for each row of the repeating section. So if your repeating section has 3 rows, idArray will be something like ["-ka1345675", "-hs7444534", "-t4igjugj158"] (obviously not real ids, but random keysmashes). The forEach function is a loop. You give it an array, it then breaks that array into its parts, and does the same thing to every one of those parts, one by one. In the original sheet worker, you have this const fieldnames = []; idArray.forEach((id) => { fieldnames.push(`repeating_weapon_${id}_weapon_dual`); }); Look at this line: idArray.forEach((id) => { This has 4 elements, an array, the forEach command, a variable, and the start of a code block (the {). an_array.forEach((temporary_variable) => { That temporary_variable can be given any name. It is, after all, temporary. forEach loops through each item in the array, stores that item in the temporary variable, then does whatever is in the code block. So we have the array ["-ka1345675", "-hs7444534", "-t4igjugj158"] and the forEach function: idArray.forEach((id) => { fieldnames.push(`repeating_weapon_${id}_weapon_dual`); }); So this breaks the array into its three parts: "-ka1345675" "-hs7444534" "-t4igjugj158" and then for each of them, creates a new string and pushes it to the field names array. So on the first iteration, it grabs the id: "-ka1345675", builds the string and pushes it to fieldnames, which now looks like this: [`repeating_weapon_-ka1345675_weapon_dual`] It then moves on to the second row id, does the same thing, so the array now looks like: [`repeating_weapon_-ka1345675_weapon_dual`, `repeating_weapon_-hs7444534_weapon_dual`] Then it grabs the 3rd row id, and repeats: [`repeating_weapon_-ka1345675_weapon_dual`, `repeating_weapon_-hs7444534_weapon_dual`, `repeating_weapon_-t4igjugj158_weapon_dual`] Then it sees there are no items left in idarray, and ends the function. The code now moves on to the getAttrs line. So a forEach loop takes an array, which is a group of things, and then does something to everything in that group, one by one. Now, remember that a sheet worker knows nothing about the attributes in a character sheet. You have to give it an array of the attributes you want to work with in getAttrs. The point of this first loop is to get the names from the repeating section you want to work with. Now after the getAttrs, youre in the body of the worker. And here you need to use a loop a second time. Remember when you write the sheet worker, you don't know how many rows the repeating section has, and the sheet worker has no way to know that either except via getSectionIDs. since the idArray was already created, you can use that to handle how many rows the section has, and can work through every row of the section.             idArray.forEach((id) => {                 const attack_type = v[`repeating_weapon_${id}_weapon_dual`];                 const thispenalty = Math.min(0, handed_mod(attack_type, dex_mod));                 output[`repeating_weapon_${id}_weapon_dual_pen`] = thispenalty;             }); So, again, it loops through every item in the idArray, assigns that to id , and does the code above. Then it goes to the second ite,, repeats, and then the third item. So in summary: getSectionIDs gives you an array of every row in the repeating section. forEach loops through every item in an array, one by one, and lets you do something with that item. Javascript has many, many ways to do loops, and foreach is probably the simplest. It is incredibly useful in sheet workers and once you understand it, you'll find lots of situations to use it. Because sheet workers are asynchronous, you usually need to loop through idArray twice: first, before getAttrs, to build a list of attribute names so it getttrs can get the values of those attributes, and the second after getAttrs, where you do something with those values.
1657474908

Edited 1657475215
vÍnce
Pro
Sheet Author
GiGs said: So in summary: Because sheet workers are asynchronous, you usually need to loop through idArray twice: first, before getAttrs, to build a list of attribute names so it getttrs can get the values of those attributes, and the second after getAttrs, where you do something with those values. This made sense and definitely helps. You rock GiGs!  I normally have a "rough" working knowledge on sheetworker js using roll20's sheetworkers wiki , help from other's(special thanks to Scott C. and GiGs !!!), other people's code(I often "borrow" as much as possible), but your explanations and examples really help to give clarity to the code.  Looking forward to your Cybersphere blog on sheetworkers. Thank you
1657480959
GiGs
Pro
Sheet Author
API Scripter
vÍnce said: Looking forward to your Cybersphere blog on sheetworkers. Thanks to you too, Vince. Honestly your questions are helping me with the blog. While answering this, I realised my current draft doesnt talk much about functions being asynchronous and the ramifications that has and how it shapes the way sheet workers are written, and I realise I need to address that.
1657585905

Edited 1657585977
John D.
Pro
Sheet Author
I leave you guys alone for a couple days and you write a novel!  :P The mathematical approach to condensing the code was a great walk-through, and I didn't know you could nest ternary statements in ternary statements.  GiGs for the win!  Usually I start out with caveman code (no insult to cavemen), and then once I got things working more or less the way I want them I take a few more passes to condense and experiment.  Most of which I learned in these forums. While I agree with GiGs that if/else statements can be cleaner to read (less code), I do find natural benefits and interesting uses for switch statements and objects.  Objects may have more nuanced uses, but if and switch statements are pretty much the same and can achieve the same results. On switch statements, I like to use these to block off code in macro frameworks.  If statements could work as well but I get confused easily.  Switch statements are easier for me to parse because it reminds me of a tree menu.  Such as: getAttr([stuff, otherStuff], v => {     const someStuff = v.stuff;     const someOtherStuff = v.otherStuff;     switch(someStuff) {         case 'test1':             doSomething();         break;         case 'test2':             doSomthingElse();         break;         default:             switch(someOtherStuff) {                 case 'test3':                     doThisInstead();                 break;                 case 'test4':                     doThisOtherThing();                 break;             }     } }); Which is exactly the same as... getAttr([stuff, otherStuff], v => { const someStuff = v.stuff; const someOtherStuff = v.otherStuff; if (someStuff === 'test1') { doSomething(); } else if (someStuff === 'test2') { doSomthingElse(); } else { if (someOtherStuff === 'test3') { doThisInstead(); } else if (someOtherStuff === 'test4') { doThisOtherThing(); } } }); The difference for me is that I don't want to keep looking at the test/evaluation, because I categorize my code blocks based on results of the test (i.e. an event) and switch statements make it easier for me to understand where in my script I am.  Inside of a switch case I may use if/else statements, and more often inside functions. As for objects, these are great for unique strings.  Such as when a string cannot be easily constructed by inserting a word in a generic string.  Such as GiGs' example: clog(`${dex_mod > 0 ? 'high' : 'low'} dex penalty: ${dex_mod}`); This will either output "dex penalty high" or "dex penalty low".  However, if you want to display one of 20 different injuries a character can take from a critical hit, for instance, storing them in an object can be convenient and reusable (because they can exist outside of a function). const critInjury = { 1: 'Lost a toe!', 2: 'Luckily you were blessed with a spare eye.', 3: 'Hope you can wipe with your left hand.', 4: 'Missing a nose somehow makes you no less attractive.', ... 20: 'and so on...' }; function doCriticalHit() { const critMessage = `Critical hit! ${critInjury[Math.floor(Math.random() * 20) + 1]}`; return critMessage; }
1657587205

Edited 1657587275
GiGs
Pro
Sheet Author
API Scripter
I agree that switches and objects can be very useful (I like using objects a lot, and have a few examples on the forums using them like tables and databases), I just wanted to show how useful else was. And with how reducible the code was, if seemed the best choice in this case.