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 question on( change and sheet

January 11 (5 years ago)

Edited January 11 (5 years ago)
vÍnce
Pro
Sheet Author

I've got a bit of code that handles some simple attribute calcs triggered using change:repeating_weapons, and this seems to work just fine as long I make a change to an attribute within repeating_weapons.  As it should. 

But, I would also like to run this function on sheet:opened as well as change:strength_total (and a few other non-repeating attributes, but I'll keep it simple for now.) 

If I add sheet:opened and change:strength_total, I get a log error 

SHEET WORKER ERROR: You attempted to set an attribute beginning with 'repeating_' but did not include a Row ID or Attribute Name in repeating_weaponseating_weapons_weapon_skill_tot
SHEET WORKER ERROR: You attempted to set an attribute beginning with 'repeating_' but did not include a Row ID or Attribute Name in repeating_weaponseating_weapons_weapon_bonus_tot
SHEET WORKER ERROR: You attempted to set an attribute beginning with 'repeating_' but did not include a Row ID or Attribute Name in repeating_weaponseating_weapons_weapon_damage_tot

I'm guessing this means I need to add some additional js to grab and insert the rowID for the setAttrs?  Is that necessary, if so, can you post an example?  If it's not necessary, how can run these calcs on repeating_weapons if any of my desired events are met? 

TIA for any help you can offer.


code that works for changes within repeating_weapons

    /* set weapon bonus and damage totals */
    on("change:repeating_weapons", function () {
        console.log(">>>> Change Detected: Weapon Attacks - recalculating totals <<<<");
        getAttrs(['repeating_weapons_weapon_skill', 'repeating_weapons_weapon_skill_tot', 'repeating_weapons_weapon_bonus', 'repeating_weapons_weapon_bonus_tot', 'repeating_weapons_weapon_misc_attack','repeating_weapons_weapon_bonus_max', 'repeating_weapons_weapon_damage', 'repeating_weapons_weapon_misc_damage', 'repeating_weapons_weapon_damage_tot', 'strength_total', 'agility_total', 'marksmanship', 'melee'], function (values) {
            const weapon_skill = parseInt(values.repeating_weapons_weapon_skill, 10) || 0,
                weapon_bonus = parseInt(values.repeating_weapons_weapon_bonus, 10) || 0,
                weapon_misc_attack = parseInt(values.repeating_weapons_weapon_misc_attack, 10) || 0,
                weapon_bonus_max = parseInt(values.repeating_weapons_weapon_bonus_max, 10) || 0,
                weapon_damage = parseInt(values.repeating_weapons_weapon_damage, 10) || 0,
                weapon_misc_damage = parseInt(values.repeating_weapons_weapon_misc_damage, 10) || 0,
                attribute = (weapon_skill === 0 ? parseInt(values.strength_total, 10) || 0 : parseInt(values.agility_total, 10) || 0),
                skill = (weapon_skill === 0 ? parseInt(values.melee, 10) || 0 : parseInt(values.marksmanship, 10) || 0),
                weapon_bonus_limit = Math.min(weapon_bonus, weapon_bonus_max),
                weapon_skill_tot = attribute + skill,
                weapon_bonus_tot = weapon_skill_tot + weapon_bonus_limit + weapon_misc_attack,
                weapon_damage_tot = weapon_damage + weapon_misc_damage;
            setAttrs({
                repeating_weapons_weapon_skill_tot : weapon_skill_tot,
                repeating_weapons_weapon_bonus_tot : weapon_bonus_tot,
                repeating_weapons_weapon_damage_tot : weapon_damage_tot
            });
            console.log('>>>> weapon_skill_tot: '+weapon_skill_tot+' weapon_bonus_tot: '+weapon_bonus_tot+' weapon_damage_tot: '+weapon_damage_tot +' <<<<');
        });
    });

code that throws the log errors (there are a couple of other non-repeating attributes, but I've left them out for now to keep it simple.)

    /* set weapon bonus and damage totals */
    on("sheet:opened change:strength_total change:repeating_weapons", function () {
        console.log(">>>> Change Detected: Weapon Attacks - recalculating totals <<<<");
        getAttrs(['repeating_weapons_weapon_skill', 'repeating_weapons_weapon_skill_tot', 'repeating_weapons_weapon_bonus', 'repeating_weapons_weapon_bonus_tot', 'repeating_weapons_weapon_misc_attack','repeating_weapons_weapon_bonus_max', 'repeating_weapons_weapon_damage', 'repeating_weapons_weapon_misc_damage', 'repeating_weapons_weapon_damage_tot', 'strength_total', 'agility_total', 'marksmanship', 'melee'], function (values) {
            const weapon_skill = parseInt(values.repeating_weapons_weapon_skill, 10) || 0,
                weapon_bonus = parseInt(values.repeating_weapons_weapon_bonus, 10) || 0,
                weapon_misc_attack = parseInt(values.repeating_weapons_weapon_misc_attack, 10) || 0,
                weapon_bonus_max = parseInt(values.repeating_weapons_weapon_bonus_max, 10) || 0,
                weapon_damage = parseInt(values.repeating_weapons_weapon_damage, 10) || 0,
                weapon_misc_damage = parseInt(values.repeating_weapons_weapon_misc_damage, 10) || 0,
                attribute = (weapon_skill === 0 ? parseInt(values.strength_total, 10) || 0 : parseInt(values.agility_total, 10) || 0),
                skill = (weapon_skill === 0 ? parseInt(values.melee, 10) || 0 : parseInt(values.marksmanship, 10) || 0),
                weapon_bonus_limit = Math.min(weapon_bonus, weapon_bonus_max),
                weapon_skill_tot = attribute + skill,
                weapon_bonus_tot = weapon_skill_tot + weapon_bonus_limit + weapon_misc_attack,
                weapon_damage_tot = weapon_damage + weapon_misc_damage;
            setAttrs({
                repeating_weapons_weapon_skill_tot : weapon_skill_tot,
                repeating_weapons_weapon_bonus_tot : weapon_bonus_tot,
                repeating_weapons_weapon_damage_tot : weapon_damage_tot
            });
            console.log('>>>> weapon_skill_tot: '+weapon_skill_tot+' weapon_bonus_tot: '+weapon_bonus_tot+' weapon_damage_tot: '+weapon_damage_tot +' <<<<');
        });
    });
January 11 (5 years ago)

Edited January 11 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

You cant use the shortened repeating section syntax with sheet opened.

Rememeber that repeating section attributes each have a full name that looks something like this:

repeating_weapons_-THEW64858956_weapon_skill

The shorter syntax like this:

repeating_weapons_weapon_skill

is only for on change events, and only affects a single row of the section. The roll20 engine recognises an attribute in the section has changed, and supplies the row id automatically, so that code becomes the full version behind the scenes.

When using sheet:opened, no row id is supplied, so the code fails.

To solve this problem when using sheet:opened, you need to use getSectionids to get all the row ids, and go through the section updating all rows that need updating.


Likewise when using change for a stat outside the repeating section, (e.g. change:strength_total), you again need to use getSectionids - because the shortened syntax only works for one row at a time. When changing an external attribute, you need to update all the rows that are affected, so you need to use getsectionids and loop through them.


Also youre using the shortened syntax in the setAttrs, which can only change one row at a time, and you probably need to update multiple rows. Again, this needs getSectionids.


Its pretty common to have one sheetworker for single row changes (triggered when a repeating_section attribute changes), and a second sheet worker for changes that affect the whole repeating section (sheet:opened, changes to external attributes).

You might move the calculation part of the worker into a function which can be called from both sheet workers, to avoid duplicated code.



January 11 (5 years ago)

Edited January 11 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

Here's how I would do it. I would create 4 sheet workers:

1 sheet worker to calculate repeating_weapons_weapon_damage_tot - this attribute is not affected by external factors (strength, agility, etc) and isnt affected by most of the repeating section attributes. It's an independent attribute of all other factors, so a dedicated sheet worker makes sense.


2 sheet workers to calculate 

repeating_weapons_weapon_skill_tot
repeating_weapons_weapon_bonus_tot 

when due to changes within the repeating section. There's an argument that you can combine these into one sheet worker. But skill_tot is made up of several attributes that only affect it, and bonus_tot is made from skill_tot and some other attributes that dont affect anything else.

So it makes sense to break these out into separate workers to me - especially since these workers only fire on changes within the repeating section and only on one row at a time. 

And finally, a sheet worker that fires when the attributes outside the sheet worker fire - strength, agility, marksmanship, and melee. This goes through the repeating section, and updates skill_tot and bonus_tot for every row. The setAttrs line uses silent: true to stop changes propagating - otherwise changing skill_tot would trigger the above repeating_section worker (#3) and force a slower recalc of all the bonus_tot lines.

This sheet worker doesnt touch the damage line, because thats not affected by strength, agility, marksmanship, and melee.



const int = (score, on_error = 0) => parseInt(score) || on_error;
 
 // make sure repeating_weapons_weapon_damage_tot isa readonly attribute and no need to use sheet opened.
on('change:repeating_weapons:weapon_damage change:repeating_weapons:weapon_misc_damage', function() {
    console.log(">>>> Change Detected: Weapon Damage - recalculating totals <<<<");
    getAttrs(['repeating_weapons_weapon_damage', 'repeating_weapons_weapon_misc_damage'], function (values) {
        const weapon_damage = int(values.repeating_weapons_weapon_damage);
        const weapon_misc_damage = int(values.repeating_weapons_weapon_misc_damage);
        
        const damage = weapon_damage + weapon_misc_damage;
        setAttrs({
            repeating_weapons_weapon_damage_tot: damage
        });
        console.log('>>>> weapon_skill_damage: ' + damage);
    });
});
// worker to calculate skill_total
on("change:repeating_weapons:weapon_skill change:repeating_weapons:weapon_bonus change:repeating_weapons:weapon_misc_attack change:repeating_weapons:weapon_bonus_max change:repeating_weapons: change:repeating_weapons: ", function () {
    console.log(">>>> Change Detected: Weapon Attacks - recalculating totals <<<<");
    // made the change event specific, so its only fired when needed, and likewise reduced the getAttrs to those only needed
    getAttrs(['repeating_weapons_weapon_skill', 'strength_total', 'agility_total', 'marksmanship', 'melee'], function (values) {
        const strength = int(values.strength_total),
            agility = int(values.agility_total),
            melee = int(values.melee),
            marksmanship = int(values.marksmanship),
            weapon_skill = int(values.repeating_weapons_weapon_skill);

        // separate out the basic values read from the sheet from those calculated within the worker. for clarity
        const attribute = (weapon_skill === 0 ? strength : agility),
            skill = (weapon_skill === 0 ? melee : marksmanship),
            weapon_skill_tot = attribute + skill;
            
        setAttrs({
            repeating_weapons_weapon_skill_tot:weapon_skill_tot
        });
        console.log('>>>> weapon_skill_tot: ' + weapon_skill_tot);
    });
});
// worker to calculate bonus_total
on("change:repeating_weapons:weapon_skill_tot change:repeating_weapons:weapon_bonus change:repeating_weapons:weapon_misc_attack change:repeating_weapons:weapon_bonus_max change:repeating_weapons: change:repeating_weapons: ", function () {
    console.log(">>>> Change Detected: Weapon Attacks - recalculating totals <<<<");
    // made the change event specific, so its only fired when needed, and likewise reduced the getAttrs to those only needed
    getAttrs(['repeating_weapons_weapon_skill_tot', 'repeating_weapons_weapon_bonus', 'repeating_weapons_weapon_misc_attack',             'repeating_weapons_weapon_bonus_max'], function (values) {
        const weapon_skill_tot = int(values.repeating_weapons_weapon_skill_tot),
            weapon_misc_attack = int(values.repeating_weapons_weapon_misc_attack),
            weapon_bonus = int(values.repeating_weapons_weapon_bonus),
            weapon_bonus_max = int(values.repeating_weapons_weapon_bonus_max);

        // separate out the basic values read from the sheet from those calculated within the worker
        const weapon_bonus_tot = weapon_skill_tot + Math.min(weapon_bonus, weapon_bonus_max) + weapon_misc_attack;
            
        setAttrs({
            repeating_weapons_weapon_bonus_tot: weapon_bonus_tot
        });
        console.log('weapon_bonus_tot: '+ weapon_bonus_tot +' <<<<');
    });
});
// calculate repeating_weapons when outside attributes change
on('change:strength_total change:agility_total change:melee change:marksmanship', function() {
    getSectionIDs('repeating_weapons', function(ids) {
        const fieldnames = [];
        ids.forEach(id => {
            fieldnames.push(
                `repeating_weapons_${id}_weapon_skill`,
                `repeating_weapons_${id}_weapon_bonus`,
                `repeating_weapons_${id}_weapon_bonus_max`,
                `repeating_weapons_${id}_weapon_misc_attack`,
            );
        });
        getAttrs(['strength_total', 'agility_total', 'melee', 'marksmanship', ...fieldnames], function(values) {
            const settings = {};
            const strength = int(values.strength_total),
                agility = int(values.agility_total),
                melee = int(values.melee),
                marksmanship = int(values.marksmanship);
                
            ids.forEach(id => {
                // calculate skill total
                const weapon_skill = int(values[`repeating_weapons_${id}_weapon_skill`]),
                    attribute = (weapon_skill === 0 ? strength : agility),
                    skill = (weapon_skill === 0 ? melee : marksmanship),
                    weapon_skill_tot = attribute + skill;
                
                //calculate bonus total
                const weapon_misc_attack = int([`values.repeating_weapons_${id}_weapon_misc_attack`]),
                    weapon_bonus = int(values[`repeating_weapons_${id}_weapon_bonus`]),
                    weapon_bonus_max = int(values[`repeating_weapons_${id}_weapon_bonus_max`]),
                    weapon_bonus_tot = weapon_skill_tot + Math.min(weapon_bonus, weapon_bonus_max) + weapon_misc_attack;

                // no need to calculate damage total, its not affected by external factors
                settings[`repeating_weapons_${id}_weapon_skill_tot`] = weapon_skill_tot;
                settings[`repeating_weapons_${id}_weapon_bonus_tot`] = weapon_bonus_tot;
            });
            setAttrs(settings, {silent: true});
        });
    });
});

I realised after writing that I could have checked the bonus_tot and skill_tot against the existing values, and only update them if they have changed. But since you are changing all the attributes at once, it's fast enough you wont notice (unless you have literally hundreds of weapons).- 


PS: you could add sheet:opened to that last worker, but if the three attributes set by these workers are readonly, you shouldnt need it. There should never be a change to the attributes that sheet:opened would be needed to fix.

January 11 (5 years ago)

Edited January 11 (5 years ago)
vÍnce
Pro
Sheet Author

Wow GiGs!  You always deliver way more than any of us can give back. ;-)

I think 90% of the sheetworker code in my projects was actually written by you.  lol

As always, I'm much appreciated for the help and explanations.

January 11 (5 years ago)

Edited January 11 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

You're welcome :)

I had a typo in the name getSectionIDs, i had typed  getSectionIDS. I always have to double check that, hehe. Fixed now.

January 11 (5 years ago)

Edited January 11 (5 years ago)
vÍnce
Pro
Sheet Author


GiGs said:

You're welcome :)

I had a typo in the name getSectionIDs, i had typed  getSectionIDS. I always have to double check that, hehe. Fixed now.


I found that.  ;-)


what does this line do?

const int = (score, on_error = 0) => parseInt(score) || on_error;

Is that a substitute or better method than using parseInt for each attribute?

example;

strength = int(values.strength_total);

instead of 

strength = parseInt(values.strength_total, 10) || 0;

January 11 (5 years ago)

Edited January 11 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

Its an alternative to writing out the full parseInt function every time you want it. Not better - it's functionally identical. It's just a lot quicker

Making it a function called int saves a lot of typing.

You can call it the normal way

const stat = int(values.stat);

and this will work like the usual parseInt command, and sets a dfault value of 0, when the stat isnt able to be read.

You do sometimes need to have a different default value from 0, so you can supply an alternative default value, like

const stat = int(values.stat, 1);

if you want the value to be 1 on a fail. That second version is the equivalent of typing

const stat = parseInt(values.stat) || 1;

That's what the on_error parameter in the int function is for.


Note that you dont need to include the ,10 part in parseInt - lots of people write parseInt like this:

const stat = parseInt(values.stat, 10) || 0;

But that's legacy syntax - the bold part isn't needed any more, and I'm not sure it ever was needed on roll20. 

January 11 (5 years ago)
vÍnce
Pro
Sheet Author

+1

January 11 (5 years ago)

Edited January 11 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

By the way, the three functions I put at the start of every character sheet's sheet workers now are 

const int = (score, on_error = 0) => parseInt(score) || on_error;
const float = (score, on_error = 0) => parseFloat(score) || on_error;
const clog = (text, title = '', color = 'green', style='font-size:12px; font-weight:normal;', headerstyle = 'font-size:13px; font-weight:bold;') => {
    let titleStyle = `color:${color}; ${headerstyle} text-decoration:underline;`;
    let textStyle = `color:${color}; ${style}`;
    let output =  `%c${title} %c${text}`;
    if(title) {
        console.log(output,titleStyle,textStyle);
    } else {         output = `%c${text}`;
        console.log(output,textStyle);
    }
};

int and float are the handy parseInt and parseFloat functions. clog is an alternative to writing console.log all the time, and lets you format the output.

If you call it like

clog('weapon_bonus_tot: '+ weapon_bonus_tot);

it will print out the line in green 12point text, making it stand out and easy to find without having to use <===== ====> type identifiers.

And this is all you need to use, but if you want extra options: 

If you call it like this

clog('repeating weapons','weapon_bonus_tot: '+ weapon_bonus_tot + 'weapon_skill_tot: ' + weapon_skill_tot);

it will print it on two lines, with Repeating_weapons as a bigger title, and the rest in smaller green text.

If you call it like

clog('weapon_bonus_tot: '+ weapon_bonus_tot, '', 'red');

it will print it out as a single line without a title, but in red instead of the default green. (You always need to include something for the second parameter (title) if you want to set a different color, but can use ' ' to set title as empty).

The 4th and 5th parameters let you replace the styling for text and title lines, if you want to set your own.

I do keep tweaking the clog function - havent found a finalised all-purpose version I'm completely happy with, but this is my current version.

January 11 (5 years ago)
vÍnce
Pro
Sheet Author

I like it.  Kind of like "shorthand" code.  I noticed that Chris B did a lot of this on the PF Community sheet.

January 11 (5 years ago)

Edited January 11 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

I wouldnt mind looking at that for ideas - the version on github uses compressed code for the sheet worker that is unreadable without the source files.

Edit: oh! i didnt realise the source was linked from the github readme. And thats a LOT of js files, lol.

January 11 (5 years ago)
vÍnce
Pro
Sheet Author

LOTS.  There are about 25+ modules of js.  I have to compile with NPM in order to upload to the repo.  I suppose the various js modules are better for such a a huge sheetworker (14k+ lines of js), but for a novice, it's very hard to follow/trace...

January 11 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

To be fair, 14,000 lines of code in a single file would be pretty hard to follow for a novice - even if it was readable!

Does NPM have the option to compile the code without minifying? It might be easier to make sense of if you were able to do that.

January 11 (5 years ago)
vÍnce
Pro
Sheet Author
The npm commands builds the html file, but puts all the js on one line. I'll have to see if there's an option to leave the js un-minified format.  I think we were hitting some limit in Roll20 before it was modified though...
January 11 (5 years ago)
GiGs
Pro
Sheet Author
API Scripter

I wonder if thats because it doesnt recognise the type="text/worker"  bit. VS Code cant format sheet workers properly in html files for this reason.

I would try changing the part of the code that creates this line

<script type="text/worker">

to just

<script>

and see if it formats it properly. If it works, you can add the type="text/worker" manually afterwards.

This is just a willd guess of course, not knowing what the process it uses is.

January 11 (5 years ago)
vÍnce
Pro
Sheet Author


GiGs said:

I wonder if thats because it doesnt recognise the type="text/worker"  bit. VS Code cant format sheet workers properly in html files for this reason.

I would try changing the part of the code that creates this line

<script type="text/worker">

to just

<script>

and see if it formats it properly. If it works, you can add the type="text/worker" manually afterwards.

This is just a willd guess of course, not knowing what the process it uses is.


PM sent since this has gotten off-topic. ;-)