You're almost certainly encountering issues caused by getAttrs and setAttrs being asynchronous functions. If you didnt see it before, it's because your sheet had a lot fewer attributes involved, so each worker finished too quickly for you to see the error. But it would have become apparent sooner or later. It's also dependent on roll20's server load, so it'll be more obvious when they are having more congested nights. Your repeating_powers function looks like this: on('change:repeating_powers remove:repeating_powers change:weight', function() { calcRepeating(); updateFromBCs();
}); calcRepeating includes a getAttrs and setAttrs call. So does updateFromBCs. The thing to understand is these functions dont necessarily finish in the order they are listed in the worker. getAttrs/setAttrs calls take time to complete - they have to contact roll2o's internet server. So calcRepeating starts, and while its running, the code continues to the next line. That code then starts. And it's getAttrs grabs attribute values from the sheet before calcRepeating has finished and updated them. So it's using old data. One of your other functions has calcRepeating, a separate getAttrs -> stAttrs section, then updatefromBCs. That's even worse - it will have three processes running, and the updateFromBCs might finish first. And these functions dont include a full set of change triggers. So when attributes on the sheet are updated, they dont necessarily trigger change events to keep anything that depends on them updated. So the main problem here is that you need to set up the change lines to include all stats that a worker might want to respond to, to keep everything in sync. Every worker that launches UpdateFromBCS need to have all these states as change events: "strength_score", "endurance_score", "agility_score", "intelligence_score", "cool_score", "init_adj_total", "hp_adj_total", "pwr_adj_total", "luck_adj_total", "weight", "ability_cp_total", "strength_cost", "endurance_cost", "agility_cost", "intelligence_cost", "cool_cost" And every worker that launches calcRepeating must have the following attributes from the repeating section as change events: "ip_cost", "st_adj", "en_adj", "ag_adj", "in_adj", "cl_adj", "init_adj","pwr_adj", "hp_adj", "luck_adj", "ability_cost" Of course you can also just have " change:repeating_powers remove :repeating_powers " instead. So you need to restructure your code somewhat. There are two main ways to do it. The first way to do it builds faster and more efficient, but is much harder to write and maintain. That is to just have one sheet worker: one giant change event which watches all stats for changes, one getAttrs which grabs all stats, one body which performs all calculations, and one setAttrs that updates all attributes. With this method you cant use sumrepeating - you need to write an embedded section which scans the repeating section, so all the calculations can be done at once. The second method is less efficient - and in some cases can be very inefficient and lead to obvious sheet lag. But your sheet isnt big enough for that to be much of an issue. It is much easier to maintain: you break the code down to smaller modular functions, and let roll20's events keep everything in sync. The main work would be reorganising your main functions into the following three: on('change:repeating_powers remove:repeating_powers', function() { repeatingSum(["abilityip_total", "st_adj_total", "en_adj_total", "ag_adj_total", "in_adj_total", "cl_adj_total","init_adj_total", "pwr_adj_total", "hp_adj_total", "luck_adj_total", "ability_cp_total"], "powers", ["ip_cost", "st_adj", "en_adj", "ag_adj", "in_adj", "cl_adj", "init_adj","pwr_adj", "hp_adj", "luck_adj", "ability_cost"]); }); const somanystats = [ "strength_score", "endurance_score", "agility_score", "intelligence_score", "cool_score", "init_adj_total", "hp_adj_total", "pwr_adj_total", "luck_adj_total", "weight", "ability_cp_total", "strength_cost", "endurance_cost", "agility_cost", "intelligence_cost", "cool_cost" ]; const somanyChanges = somanystats.map(stat => `change:${stat}`).join(' ');
on(`${somanyChanges}`, function() { getAttrs(somanystats, function(v) { let statTable = [ {min: 0, max: 0, carry: 8, hth_init: 'd2-1', save: 6, hits_st: -3, hits_en: -5, hits_ag: -2, hits_cl: -1, heal: .2}, {min: 1, max: 1, carry: 10, hth_init: 'd2-1', save: 7, hits_st: -3, hits_en: -5, hits_ag: -2, hits_cl: -1, heal: .3}, {min: 2, max: 2, carry: 12, hth_init: 'd2-1', save: 7, hits_st: -3, hits_en: -5, hits_ag: -2, hits_cl: -1, heal: .3}, {min: 3, max: 5, carry: 15, hth_init: 'd2', save: 8, hits_st: -2, hits_en: -3, hits_ag: -1, hits_cl: -0, heal: .5}, {min: 6, max: 8, carry: 30, hth_init: 'd3', save: 9, hits_st: 0, hits_en: -1, hits_ag: 0, hits_cl: 1, heal: .8}, {min: 9, max: 11, carry: 60, hth_init: 'd4', save: 10, hits_st: 1, hits_en: 1, hits_ag: 1, hits_cl: 1, heal: 1}, {min: 12, max: 14, carry: 120, hth_init: 'd6', save: 11, hits_st: 3, hits_en: 3, hits_ag: 2, hits_cl: 2, heal: 1.6}, {min: 15, max: 17, carry: 240, hth_init: 'd6+1', save: 11, hits_st: 5, hits_en: 6, hits_ag: 3, hits_cl: 2, heal: 2.2}, {min: 18, max: 20, carry: 480, hth_init: 'd8+1', save: 12, hits_st: 6, hits_en: 8, hits_ag: 5, hits_cl: 3, heal: 2.8}, {min: 21, max: 23, carry: 960, hth_init: 'd10+1', save: 12, hits_st: 8, hits_en: 10, hits_ag: 6, hits_cl: 3, heal: 3.4}, {min: 24, max: 26, carry: 1920, hth_init: '2d6', save: 13, hits_st: 10, hits_en: 13, hits_ag: 7, hits_cl: 5, heal: 3.9}, {min: 27, max: 29, carry: 3840, hth_init: 'd6+d8', save: 13, hits_st: 12, hits_en: 15, hits_ag: 8, hits_cl: 5, heal: 4.5}, {min: 30, max: 32, carry: 7680, hth_init: '2d8', save: 14, hits_st: 14, hits_en: 17, hits_ag: 9, hits_cl: 5, heal: 5.1}, {min: 33, max: 35, carry: 15360, hth_init: 'd8+d10', save: 14, hits_st: 16, hits_en: 20, hits_ag: 10, hits_cl: 6, heal: 5.7}, {min: 36, max: 38, carry: 30720, hth_init: '2d10', save: 15, hits_st: 17, hits_en: 22, hits_ag: 12, hits_cl: 6, heal: 6.3}, {min: 39, max: 41, carry: 61440, hth_init: 'd10+d12', save: 15, hits_st: 19, hits_en: 25, hits_ag: 13, hits_cl: 7, heal: 6.9}, {min: 42, max: 44, carry: 122880, hth_init: '2d12', save: 16, hits_st: 21, hits_en: 27, hits_ag: 14, hits_cl: 7, heal: 7.5}, {min: 45, max: 47, carry: 245760, hth_init: '3d8', save: 16, hits_st: 23, hits_en: 29, hits_ag: 15, hits_cl: 8, heal: 8.1}, {min: 48, max: 50, carry: 491520, hth_init: '2d8+d10', save: 17, hits_st: 25, hits_en: 32, hits_ag: 16, hits_cl: 9, heal: 8.7}, {min: 51, max: 53, carry: 983040, hth_init: 'd8+2d10', save: 17, hits_st: 27, hits_en: 34, hits_ag: 17, hits_cl: 9, heal: 9.2}, {min: 54, max: 56, carry: 1966080, hth_init: '3d10', save: 18, hits_st: 28, hits_en: 36, hits_ag: 19, hits_cl: 10, heal: 9.8}, {min: 57, max: 59, carry: 3932160, hth_init: '2d10+d12', save: 18, hits_st: 30, hits_en: 39, hits_ag: 20, hits_cl: 10, heal: 10.4}, {min: 60, max: 62, carry: 7864320, hth_init: 'd10+2d12', save: 19, hits_st: 32, hits_en: 41, hits_ag: 21, hits_cl: 11, heal: 11}, {min: 63, max: 65, carry: 15728640, hth_init: '3d12', save: 19, hits_st: 34, hits_en: 43, hits_ag: 22, hits_cl: 12, heal: 11.6}, {min: 66, max: 68, carry: 31457280, hth_init: '3d12+1', save: 20, hits_st: 36, hits_en: 46, hits_ag: 23, hits_cl: 12, heal: 12.2}, {min: 69, max: 71, carry: 62914560, hth_init: '3d12+2', save: 20, hits_st: 38, hits_en: 48, hits_ag: 25, hits_cl: 13, heal: 12.8}, {min: 72, max: 74, carry: 125829120, hth_init: '4d10', save: 21, hits_st: 39, hits_en: 50, hits_ag: 26, hits_cl: 13, heal: 13.4}, {min: 75, max: 77, carry: 251658240, hth_init: '3d10+d12', save: 21, hits_st: 41, hits_en: 53, hits_ag: 27, hits_cl: 14, heal: 14}, {min: 78, max: 80, carry: 503316480, hth_init: '2d10+2d12', save: 22, hits_st: 43, hits_en: 55, hits_ag: 28, hits_cl: 15, heal: 14.5}, {min: 81, max: 83, carry: 1006632960, hth_init: 'd10+3d12', save: 22, hits_st: 45, hits_en: 58, hits_ag: 29, hits_cl: 15, heal: 15.1}, {min: 84, max: 86, carry: 2013265920, hth_init: '4d12', save: 23, hits_st: 47, hits_en: 60, hits_ag: 30, hits_cl: 16, heal: 15.7}, {min: 87, max: 89, carry: 4026531840, hth_init: '4d12+1', save: 23, hits_st: 48, hits_en: 62, hits_ag: 32, hits_cl: 16, heal: 16.3}, {min: 90, max: 92, carry: 8053063680, hth_init: '5d10', save: 24, hits_st: 50, hits_en: 65, hits_ag: 33, hits_cl: 17, heal: 16.9}, {min: 93, max: 95, carry: 16106127360, hth_init: '4d10+d12', save: 24, hits_st: 52, hits_en: 67, hits_ag: 34, hits_cl: 17, heal: 17.5}, {min: 96, max: 98, carry: 32212254720, hth_init: '3d10+2d12', save: 25, hits_st: 54, hits_en: 69, hits_ag: 35, hits_cl: 18, heal: 18.1} ]; let stResult = statTable.filter(tableRow => (tableRow.min <= v.strength_score && tableRow.max >= v.strength_score)); let enResult = statTable.filter(tableRow => (tableRow.min <= v.endurance_score && tableRow.max >= v.endurance_score)); let agResult = statTable.filter(tableRow => (tableRow.min <= v.agility_score && tableRow.max >= v.agility_score)); let inResult = statTable.filter(tableRow => (tableRow.min <= v.intelligence_score && tableRow.max >= v.intelligence_score)); let clResult = statTable.filter(tableRow => (tableRow.min <= v.cool_score && tableRow.max >= v.cool_score)); let leap = (parseInt(v.weight,10) > 0) ? Math.floor(stResult[0].carry / parseInt(v.weight,10)) : ''; console.log(v.ability_cp_total); console.log(v.luck_adj_total); setAttrs({ carry_capacity: stResult[0].carry, endurance_save: enResult[0].save, agility_save: agResult[0].save, intelligence_save: inResult[0].save, cool_save: clResult[0].save, hth_damage: stResult[0].hth_init, initiative_score: simplifyDice(clResult[0].hth_init + '+' + v.init_adj_total), luck: 10+(parseInt(v.luck_adj_total,10) || 0), power_score_max: (parseInt(v.strength_score,10) || 0) + (parseInt(v.endurance_score,10) || 0) + (parseInt(v.agility_score,10) || 0) + (parseInt(v.intelligence_score,10) || 0) + (parseInt(v.pwr_adj_total,10) || 0), hits_score_max: stResult[0].hits_st + enResult[0].hits_en + agResult[0].hits_ag + clResult[0].hits_cl + (parseInt(v.hp_adj_total,10) || 0), movement_leap: leap, healing_rate: enResult[0].heal, power_level: (parseInt(v.ability_cp_total,10) || 0) + (parseInt(v.strength_cost,10) || 0) + (parseInt(v.agility_cost,10) || 0) + (parseInt(v.endurance_cost,10) || 0) + (parseInt(v.intelligence_cost,10) || 0) + (parseInt(v.cool_cost,10) || 0) }); }); }; ["strength", "endurance", "agility", "intelligence", "cool"].forEach(stat => { let stat_cost = `${stat}_cost`, stat_adj = (stat == 'cool') ? `cl_adj_total` : `${stat.slice(0,2)}_adj_total`; on(`change:${stat_cost} change:${stat_adj}`, function() { getAttrs([stat_cost, stat_adj], function(v) { let cost = +v[stat_cost] ||0; let adj = +v[stat_adj] ||0; let score = cost + adj; setAttrs({ [`${stat}_score`]: score }); }); }); }); Notice that each function includes only one getAttrs and setAttrs, and doesnt call any other processes that contain those functions. This allows roll20's events (the change:stat lines) to keep everything synchronised. When one worker runs, and runs setAttrs, the sheet gets updated. When the sheet gets updated, any new events trigger, and they update the sheet again. That triggers new events. And this process continues until the sheet is fully updated. The longer that chain takes the more inefficient the sheet is - but in your sheet that should never take make than 2 or 3 passes. The thing that keeps everything in sync: the sheet workers are always using the most up-to-date versions of the attributes. A setAttrs runs, triggering a new getAttrs - but that getAttrs has the values of the attributes that just changed. Your current sheet doesnt run like that, and thats where the error comes in. I threw the functions above together very quickly,making the smallest changes I could to the existing functions. You'll need to check they work properly. And make sure to remove the functions they replace.