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

TheAaron

Has anybody seen TheAaron? I messaged him a few times in the last few weeks, but he has not replied...wondering if he is ok?
1562200567
GiGs
Pro
Sheet Author
API Scripter
He's been on the forum this week, maybe he has just missed your messages. 
GiGs said: He's been on the forum this week, maybe he has just missed your messages.  Thanks Gigs...I'm really struggling with a problem and I have no idea how to fix it.  I messaged him several times in the last month and added him as GM to my game but he seemed to just drop off the radar... I'll try him again. How are you at Custom sheets and fixing code?  :P Wanna take a look?  ;)
1562208621
GiGs
Pro
Sheet Author
API Scripter
He did say on the forum he was going through quite a busy patch.  I cant give personal help, but i suggest opening a thread with your issue and we (the collective forum) may be able to help.
1562208778
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
GiGs is pretty mighty.
1562210592
Victor B.
Pro
Sheet Author
API Scripter
Or pray to The Aaron.  That usually works.  
1562210737
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
I just "spoke" to him earlier this evening. He's here; he just leads a busy existence.
1562211405
The Aaron
Roll20 Production Team
API Scripter
It's true. I'm even going to work tomorrow despite the holiday! Sorry for the delay, I'll try to take a look this weekend. Incidentally, I don't read the Character sheets forum, so it's easier to get my attention in API or Pro. I also don't mind if you keep pinging me on that PM thread. That will send me an email and bump my unread messages, which is a great help with keeping things fresh in my mind. =D
1562211560
Victor B.
Pro
Sheet Author
API Scripter
Ask and ye shall receive.....
ahhhh....the collective mighty have spoken  :P Actually...I have made some progress in diagnosing the problem. The HeroTracker API looks for attributes called "SPD" and "DEX". var speed_field = "SPD"; var dex_field = "DEX"; var speed; // if undefined, get value from the token var dex;  // if undefined, get value from the token My HTML defines attributes "DEX" and "SPD" as follows...             <input class="input40 center" type="number" name="attr_DEX" disabled="true" value="(round(@{dexxp}/3)+10)">     <input class="input40 center" type="number" name="attr_SPD" disabled="true" value="floor(@{DEX}/10+(@{spdxp}/10))+1"> But for a reason that escapes me, the attributes are being defined within the sheet as "dex" and "spd" as evidenced below...      As a result, the HeroTracker API is failing with the following error: Your scripts are currently disabled due to an error that was detected. Please make appropriate changes to your scripts and click the "Save Script" button and we'll attempt to start running them again.  More info... For reference, the error message generated was:  TypeError: Cannot read property '1' of undefined TypeError: Cannot read property '1' of undefined at Object.addToTracker (apiscript.js:4311:30) at apiscript.js:4113:19 at Function._.each._.forEach (/home/node/d20-api-server/node_modules/underscore/underscore.js:186:9) at Object.handleTrackerMessage (apiscript.js:4096:7) at handleChatMessage (apiscript.js:4444:23) at eval (eval at <anonymous> (/home/node/d20-api-server/api.js:151:1), <anonymous>:65:16) at Object.publish (eval at <anonymous> (/home/node/d20-api-server/api.js:151:1), <anonymous>:70:8) at /home/node/d20-api-server/api.js:1634:12 at /home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:560 at hc (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:39:147)
1562230477
Finderski
Plus
Sheet Author
Compendium Curator
Is there a reason you need those attributes capitalized? I ask, because because even sheet worker events we need to put them in lowercase, the only time we use the proper case with a sheet worker is when we are getting the attribute values or setting the value to something.  I've found it's easier to just use lowercase all the time...
1562230797

Edited 1562230957
Ray
Pro
Finderski said: Is there a reason you need those attributes capitalized? I ask, because because even sheet worker events we need to put them in lowercase, the only time we use the proper case with a sheet worker is when we are getting the attribute values or setting the value to something.  I've found it's easier to just use lowercase all the time... I don't need the attributes capitalized...the HeroTracker API needs them that way. In fact, that is the reason I got into this whole bloody mess in the first place. I tried to change everything to lowercase and it all shit itself and I have been trying to fix this nonsense for 2 months  :( I have come to realize that if you make a change to your custom sheet, the change does not affect any pre-existing sheets. I think that this also contributed to the problem I am having, because it made diagnosing the error difficult.
1562259082
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
so, I'm not familiar with the herotracker api, but assuming that it's using findObjs to look for attributes named spd and dex, you could just add the case insensitive toggle to the findObjs call: let spd = findObjs({name:'spd',characterid:some_id},{caseInsensitive:true}); and then it'll find attributes spd, sPd, SPD, etc.
1562265787
GiGs
Pro
Sheet Author
API Scripter
Thats a good idea. You'd have to make sure you dont have any duplicate dex and spd values on your character sheet, which i suspect might be the source of your issue anyway. Does the problem persist with completely new characters?
1562290669
The Aaron
Roll20 Production Team
API Scripter
I threw together this script to let you check.  Just run this command: !find-dup-attrs And it will generate output like this: Next we can see about cleaning them up... on('ready',()=>{ on('chat:message', (msg)=>{ if('api'===msg.type && playerIsGM(msg.playerid) && /!find-dup-attrs\b/i.test(msg.content)){ let CharAttrMap = findObjs({ type:'attribute' }).reduce((m,a)=>{ let cid = a.get('characterid'); let key = a.get('name').toLowerCase(); m[cid]=m[cid]||{}; m[cid][key]=m[cid][key]||{}; m[cid][key][a.id]=a; return m; },{}); let DupCharAttrMap = Object.keys(CharAttrMap).reduce( (m,cid)=>{ Object.keys(CharAttrMap[cid]).forEach(key=>{ if(Object.keys(CharAttrMap[cid][key]).length>1){ m[cid]=m[cid]||{}; m[cid][key]=Object.values(CharAttrMap[cid][key]); } }); return m; },{}); let output=[]; const s = { outer: 'margin: .1em;margin-top:.5em; border: 1px solid #999; padding: .1em; border-radius: .1em; background-color: #eee;', char: 'font-weight: bold; font-size: 1.5em; border-bottom: 2px solid #933;', id: 'font-weight: bold; font-size: .8em;', val: 'display:inline-block; padding: .1em .2em; max-width: 13em; max-height:1.2em; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; background-color: #eec; border: 1px solid #333;', attr: 'padding-left: 3em; font-size: .8em;', key: 'padding-left: 1.5em;', dup: 'border-bottom: 1px dashed #666;margin-bottom: .1em;' }; const stripHTML = (t) => t.replace(/<br\/?>/gi,'\n').replace(/<\/div>/gi,'\n').replace(/<[^>]*?>/g,''); const HE = (() => { const esRE = (s) => s.replace(/(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g,'\\$1'); const e = (s) => `&${s};`; const entities = { '<' : e('lt'), '>' : e('gt'), "'" : e('#39'), '@' : e('#64'), '{' : e('#123'), '|' : e('#124'), '}' : e('#125'), '[' : e('#91'), ']' : e('#93'), '"' : e('quot') }; const re = new RegExp(`(${Object.keys(entities).map(esRE).join('|')})`,'g'); return (s) => s.replace(re, (c) => (entities[c] || c) ); })(); const f = { outer: (o)=>`<div style='${s.outer}'>${o}</div>`, char: (c)=>`<div style='${s.char}'>${c.get('name')}</div>`, val: (v)=>`<span style='${s.val}'>${HE(stripHTML(`${v}`).replace(/\r\n|\r|\n/g,' '))||' '}</span>`, id: (id)=>`<span style='${s.id}'>[<code>${id}</code>]</span>`, attr: (a)=>`<div style='${s.attr}'><b>${a.get('name')}</b> ${f.id(a.id)} ${f.val(a.get('current'))} <b>/</b> ${f.val(a.get('max'))}</div>`, key: (k)=>`<div style='${s.key}'>${k}</div>`, dup: (k,as)=>`<div style='${s.dup}'>${f.key(k)}${as.map(f.attr).join('')}</div>`, dups: (das)=>Object.keys(das).map((k)=>f.dup(k,das[k])).join('') }; Object.keys(DupCharAttrMap).forEach( cid =>{ let c = getObj('character',cid); if(c){ output.push(f.outer(`${f.char(c)}${f.dups(DupCharAttrMap[cid])}`)); } }); sendChat('',`/w gm ${output.join('')}`); } }); });
1562295148
The Aaron
Roll20 Production Team
API Scripter
Ok, here's an enhanced version that has several options for fixing duplicate attributes: First off, I added the --fix-equal argument, which will automatically get rid of all duplicates if they all have the same current and max value: !find-dup-attrs --fix-equal It will retain the first one listed (which is very likely the oldest one).  You can additionally specify one of --force-lower-case or --force-upper-case: !find-dup-attrs --fix-equal --force-lower-case !find-dup-attrs --fix-equal --force-upper-case And it will additionally change the case of the retained attribute to be either lower case or upper case as specified (if you specify both, it will lowercase). For fixing duplicates when they values vary, I added several buttons: It will not ask you for confirmation, it will just do what you tell it to, so be sure you mean it. It will give you output to tell you what it did: Hope that helps fix things! Code: on('ready',()=>{ const s = { outer: 'margin: .1em;margin-top:.5em; border: 1px solid #999; padding: .1em; border-radius: .1em; background-color: #eee;', outerLabel: 'color:#ff0000;font-weight:bold;text-align:center;background-color:#ffff00;', char: 'font-weight: bold; font-size: 1.5em; border-bottom: 2px solid #933;', id: 'font-weight: bold; font-size: .8em;', val: 'display:inline-block; padding: .1em .2em; max-width: 13em; max-height:1.2em; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; background-color: #eec; border: 1px solid #333;', attr: 'padding-left: 3em; font-size: .8em;', key: 'padding-left: 1.5em;', dup: 'border-bottom: 1px dashed #666;margin-bottom: .1em;', btn: 'border: 1px solid #ccc; padding: .25em; border-radius: 50%; background-color: #669966;min-width:1.5em;min-height:1.5em;text-align:center; font-weight:bold;', btn_L: 'background-color: #5bc0de; border-color: #45b8da;', btn_U: 'background-color: #5cb85c; border-color: #4cae4c;', btn_O: 'background-color: #f0ad4e; border-color: #eea236;', btn_X: 'background-color: #d9534f; border-color: #d43f3a;' }; const stripHTML = (t) => t.replace(/<br\/?>/gi,'\n').replace(/<\/div>/gi,'\n').replace(/<[^>]*?>/g,''); const HE = (() => { const esRE = (s) => s.replace(/(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g,'\\$1'); const e = (s) => `&${s};`; const entities = { '<' : e('lt'), '>' : e('gt'), "'" : e('#39'), '@' : e('#64'), '{' : e('#123'), '|' : e('#124'), '}' : e('#125'), '[' : e('#91'), ']' : e('#93'), '"' : e('quot') }; const re = new RegExp(`(${Object.keys(entities).map(esRE).join('|')})`,'g'); return (s) => s.replace(re, (c) => (entities[c] || c) ); })(); const f = { outer: (o)=>`<div style='${s.outer}'>${o}</div>`, outerLabel: (l,o)=>f.outer(`<div style='${s.outerLabel}'>${l}</div> ${o}`), char: (c)=>`<div style='${s.char}'>${c.get('name')}</div>`, val: (v)=>`<span style='${s.val}'>${HE(stripHTML(`${v}`).replace(/\r\n|\r|\n/g,' '))||' '}</span>`, id: (id)=>`<span style='${s.id}'> ${f.lowerBtn(id)}${f.upperBtn(id)}${f.onlyBtn(id)}${f.deleteBtn(id)} [<code>${id}</code>]</span>`, attr: (a)=>`<div style='${s.attr}'><b>${a.get('name')}</b> ${f.id(a.id)} ${f.val(a.get('current'))} <b>/</b> ${f.val(a.get('max'))}</div>`, key: (k)=>`<div style='${s.key}'>${k}</div>`, dup: (k,as)=>`<div style='${s.dup}'>${f.key(k)}${as.map(f.attr).join('')}</div>`, dups: (das)=>Object.keys(das).map((k)=>f.dup(k,das[k])).join(''), btn: (l,i)=>`<a style='${s.btn}${s[`btn_${l}`]}' href='${i}'>${l}</a>`, lowerBtn: (id)=>f.btn('L',`!lower-case-attr-by-id ${id}`), upperBtn: (id)=>f.btn('U',`!upper-case-attr-by-id ${id}`), onlyBtn: (id)=>f.btn('O',`!only-this-attr-by-id ${id}`), deleteBtn: (id)=>f.btn('X',`!delete-attr-by-id ${id}`) }; on('chat:message', (msg)=>{ if('api'===msg.type && playerIsGM(msg.playerid) ) { if(/!find-dup-attrs\b/i.test(msg.content)){ let args = msg.content.split(/\s+/).map(s=>s.toLowerCase()); let fixEqual = args.includes('--fix-equal'); let forceLowerCase = args.includes('--force-lower-case'); let forceUpperCase = args.includes('--force-upper-case'); let CharAttrMap = findObjs({ type:'attribute' }).reduce((m,a)=>{ let cid = a.get('characterid'); let key = a.get('name').toLowerCase(); m[cid]=m[cid]||{}; m[cid][key]=m[cid][key]||{}; m[cid][key][a.id]=a; return m; },{}); let DupCharAttrMap = Object.keys(CharAttrMap).reduce( (m,cid)=>{ Object.keys(CharAttrMap[cid]).forEach(key=>{ if(Object.keys(CharAttrMap[cid][key]).length>1){ m[cid]=m[cid]||{}; m[cid][key]=Object.values(CharAttrMap[cid][key]); } }); return m; },{}); let fixed = {}; if(fixEqual){ Object.keys(DupCharAttrMap).forEach(cid=>{ Object.keys(DupCharAttrMap[cid]).forEach(key=>{ let same = true; let current = DupCharAttrMap[cid][key][0].get('current'); let max = DupCharAttrMap[cid][key][0].get('max'); DupCharAttrMap[cid][key].slice(1).forEach(a=>{ same = same && (current === a.get('current')); same = same && (max === a.get('max')); }); if(same){ DupCharAttrMap[cid][key].slice(1).forEach(a=>a.remove()); if(forceLowerCase){ DupCharAttrMap[cid][key][0].set({ name: DupCharAttrMap[cid][key][0].get('name').toLowerCase() }); } else if( forceUpperCase){ DupCharAttrMap[cid][key][0].set({ name: DupCharAttrMap[cid][key][0].get('name').toUpperCase() }); } fixed[cid]=fixed[cid]||{}; fixed[cid][key]=[DupCharAttrMap[cid][key][0]]; delete DupCharAttrMap[cid][key]; } }); }); } if(Object.keys(DupCharAttrMap).length){ let output=[]; Object.keys(DupCharAttrMap).forEach( cid =>{ let c = getObj('character',cid); if(c){ output.push(f.outer(`${f.char(c)}${f.dups(DupCharAttrMap[cid])}`)); } }); sendChat('',`/w gm ${output.join('')}`); } else { sendChat('',`/w gm No duplicate attributes found.`); } if(Object.keys(fixed).length){ let output=[]; Object.keys(fixed).forEach( cid =>{ let c = getObj('character',cid); if(c){ output.push(f.outerLabel('Fixed',`${f.char(c)}${f.dups(fixed[cid])}`)); } }); sendChat('',`/w gm ${output.join('')}`); } } if(/!lower-case-attr-by-id/i.test(msg.content)){ let args = msg.content.split(/\s+/); let a = getObj('attribute', args[1]); if(a){ a.set({ name: a.get('name').toLowerCase() }); let c = getObj('character',a.get('characterid')); if(c){ sendChat('',`/w gm ${ f.outerLabel('Lowercased',`${f.char(c)}${f.dup(a.get('name'),[a])}`) }`); } } } if(/!upper-case-attr-by-id/i.test(msg.content)){ let args = msg.content.split(/\s+/); let a = getObj('attribute', args[1]); if(a){ a.set({ name: a.get('name').toUpperCase() }); let c = getObj('character',a.get('characterid')); if(c){ sendChat('',`/w gm ${ f.outerLabel('Uppercased',`${f.char(c)}${f.dup(a.get('name').toLowerCase(),[a])}`) }`); } } } if(/!delete-attr-by-id/i.test(msg.content)){ let args = msg.content.split(/\s+/); let a = getObj('attribute', args[1]); if(a){ let c = getObj('character',a.get('characterid')); if(c){ sendChat('',`/w gm ${ f.outerLabel('Deleted',`${f.char(c)}${f.dup(a.get('name').toLowerCase(),[a])}`) }`); a.remove(); } } } if(/!only-this-attr-by-id/i.test(msg.content)){ let args = msg.content.split(/\s+/); let a = getObj('attribute', args[1]); if(a){ let c = getObj('character',a.get('characterid')); if(c){ let key = a.get('name').toLowerCase(); let attrs = findObjs({ type: 'attribute', characterid: c.id }).filter(a2=>a2.get('name').toLowerCase()===key && a2.id !==a.id); sendChat('',`/w gm ${ f.outerLabel(`Only This Attr Saved, ${attrs.length} Deleted`,`${f.char(c)}${f.dup(a.get('name').toLowerCase(),[a])}`) }`); attrs.forEach(a2=>a2.remove()); } } } } }); });
1562370495

Edited 1562371576
Ray
Pro
errrr....rather than add all that extra code to check for duplicate attributes, I have just copied the HTML to a text file and used a "find"/"find next" command...I can't see any duplicates of the attributes in question. Does this accomplish the same thing as the above code? or am I not understanding something? (highly likely)
1562371500

Edited 1562371556
GiGs
Pro
Sheet Author
API Scripter
You wont see duplicate attributes in the html, they aren't stored there. They are stored in a backend database that you need scripts to access. Adding a script like the one above wont affect the game's performance, and you can disable or delete it after use. That script looks amazing, Aaron. Very handy.
GiGs said: You wont see duplicate attributes in the html, they aren't stored there. They are stored in a backend database that you need scripts to access. Adding a script like the one above wont affect the game's performance, and you can disable or delete it after use. That script looks amazing, Aaron. Very handy. Ahhhh...I understand GiGs....another question though...when I look at attribute names via the token...will I be able to see duplicates there? for example...
GiGs...also...if I just add a new sheet, surely it will not have any duplicate attributes?? I mean, I understand that any existing character sheets may have this problem, but any new sheet I add will surely not?
1562372276
GiGs
Pro
Sheet Author
API Scripter
I honestly dont know. But seriously, your best bet is to try out the script. It'll just take a minute to install, then just type  !find-dup-attrs in chat, and it'll tell you if there are dupes. 
1562372413
GiGs
Pro
Sheet Author
API Scripter
Ray said: GiGs...also...if I just add a new sheet, surely it will not have any duplicate attributes?? I mean, I understand that any existing character sheets may have this problem, but any new sheet I add will surely not? I would expect a new sheet shouldnt have duplicates, but there might be something funky going on. There's no harm in trying this out just to eliminate the possibility. If you dont try it, we'll always be wondering, and it'll limit the help we can give.
GiGs said: I honestly dont know. But seriously, your best bet is to try out the script. It'll just take a minute to install, then just type  !find-dup-attrs in chat, and it'll tell you if there are dupes.  Ok...fair enough...the script Aaron wrote...do I just add it to the bottom of my HTML? Sorry to be a dunce, but his script is way beyond my understanding...
1562373236
GiGs
Pro
Sheet Author
API Scripter
No, it gets added to the API Scripts page of your campaign: Click the settings button, and select API Scripts from the dropdown. On the new page that opens, select New Script, and then, in that page: Add a name to the small box at the top where it sayes untitled.js  (the name can be anything) Copy the script into the bigger black box below it Click the Save Script button beneath it. And there you go, you can go back to your campaign and use it.
Thanks GiGs  :)
1562376683

Edited 1562376745
Ray
Pro
Ok...I ran the script and I got the following.... (From ):  No duplicate attributes found. So...now I'm really stumped  :(
1562377377
The Aaron
Roll20 Production Team
API Scripter
Well, at least we can scratch that one off the list. 
EUREKA!!!!!! I have discovered something important!!!! When I manually add the dex & spd attributes (case works both ways) to the attributes list on the "Attributes & Abilities" tab of the character sheet, the HeroTracker API works!!!
1562379464
The Aaron
Roll20 Production Team
API Scripter
Woot!
Therefore....I conclude that the HeroTracker API looks for the "dex" and "spd" attributes on the "Attributes & Abilities" tab.....it does NOT look at the attributes stored automatically by the system!!!
1562379654
The Aaron
Roll20 Production Team
API Scripter
That's the only place attributes go (with the exception of repeating attributes, which are hidden). It's more likely that the character sheet doesn't cause those attributes to exist for some reason, meaning the manual adding (or adding by API) is necessary. 
The Aaron said: That's the only place attributes go (with the exception of repeating attributes, which are hidden). It's more likely that the character sheet doesn't cause those attributes to exist for some reason, meaning the manual adding (or adding by API) is necessary.  Understood... except that before I discovered this solution, I could see the "dex" and "spd" attributes when I looked at the token bubble value options...
1562380147
The Aaron
Roll20 Production Team
API Scripter
Character sheets can have "autocalc" fields that have a value and can be tied to bars and references in formulas, but have no attribute associated with them. That's what you're seeing there. They are a pain for the api as they are completely invisible. The only way to get their value is with the "getattrbyname()" function, which has its own problems. 
The Aaron said: Character sheets can have "autocalc" fields that have a value and can be tied to bars and references in formulas, but have no attribute associated with them. That's what you're seeing there. They are a pain for the api as they are completely invisible. The only way to get their value is with the "getattrbyname()" function, which has its own problems.  Ahhhh...ok...so how do I "force" the creation of an actual Attribute from an autocalc ? I anticipate the only way is with a sheetworker script?
1562380598
The Aaron
Roll20 Production Team
API Scripter
Very likely. Somebody (GiGs? Scott C.?) probably knows about it than I do. 
1562380912
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Hmm, I'm not actually sure there's a way to handle those. Although if you remove the autocalc from the html code, it should remove the autocalc version. Really though, I'd recommend replacing all your autocalcs with sheetworker driven calcs. Autocalcs really big a sheet down.
Scott C. said: Hmm, I'm not actually sure there's a way to handle those. Although if you remove the autocalc from the html code, it should remove the autocalc version. Really though, I'd recommend replacing all your autocalcs with sheetworker driven calcs. Autocalcs really big a sheet down. Going to message you Scott...you may just turn out to be my new best friend  ;)  :P
1562381092

Edited 1562381167
GiGs
Pro
Sheet Author
API Scripter
Ray said: EUREKA!!!!!! I have discovered something important!!!! When I manually add the dex & spd attributes (case works both ways) to the attributes list on the "Attributes & Abilities" tab of the character sheet, the HeroTracker API works!!! Wow, we should have investigated this first. It's actually a long-standing issue that autocalc fields dont work well with scripts.  There are ways to fix this with sheet workers. The simplest is to remove the autocalc and replace with a sheet worker. If you post your input code for the dex and spd attributes, I'll show you how to do that. Edit: ninja'd. :)
GiGs said: Ray said: EUREKA!!!!!! I have discovered something important!!!! When I manually add the dex & spd attributes (case works both ways) to the attributes list on the "Attributes & Abilities" tab of the character sheet, the HeroTracker API works!!! Wow, we should have investigated this first. It's actually a long-standing issue that autocalc fields dont work well with scripts.  There are ways to fix this with sheet workers. The simplest is to remove the autocalc and replace with a sheet worker. If you post your input code for the dex and spd attributes, I'll show you how to do that. Edit: ninja'd. :) ok GiGs...here goes         <!-- DEFINE DEXTERITY -->         <div>             <div class="field40 right">DEX</div>             <div class="field10"></div>             <div class="field45 center">10</div>             <div class="field45 center">x3</div>             <input class="input30 center" type="number" name="attr_dexxp" value="0">             <div class="field45 center">20</div>             <div class="field10"></div>             <input class="input40 center" type="number" name="attr_dex" disabled="true" value="(round(@{dexxp}/3)+10)">         </div> and....         <!-- DEFINE SPEED --> <div>             <div class="field40 right">SPD</div>             <div class="field10"></div>             <div class="field85b center">1+(DEX/10)</div>     <div class="field45 center">x10</div>             <input class="input30 center" type="number" name="attr_spdxp" value="0">             <div class="field45 center">4</div>             <div class="field10"></div>             <input class="input40 center" type="number" name="attr_spd" disabled="true" value="floor(@{dex}/10+(@{spdxp}/10))+1">         </div>
1562385676

Edited 1562386552
GiGs
Pro
Sheet Author
API Scripter
First step: change the input lines to these versions: <input class="input40 center" type="number" name="attr_dex" value="10" readonly> <input class="input40 center" type="number" name="attr_spd" value="2" readonly> readonly works like disabled in stopping players from altering the values, but lets sheet workers change them.  Next, if you dont have a script block in your html (at the beginning or the end, your preference), create one. it looks like: <script type="text/worker"> </script> Any and all sheet workers you create go between those two script tags. Now add these two sheet workers into that script block:     on('change:dex change:spdxp sheet:opened', function () { getAttrs(['dex','spdxp'], function(v) { const dex = v.dex *1||0; const spdxp = v.spdxp *1||0; const spd = Math.floor(dex/10 + spdxp/10); setAttrs({ spd: spd }); }); });     on('change:dexxp sheet:opened', function () { getAttrs(['dexxp'], function(v) { const dexxp = v.dexxp *1||0; const dex = Math.round(dexxp/3 + 10); setAttrs({ dex: dex }); }); }); They look complicated, and I can explain the layout a bit later (too late for today).  I make these workers a little more verbose than necessary, to make it easier to explain things, to check for errors, and for ease of copying to other sheet workers. You'll find that most of the time, the bulk of sheet workers is identical, and you only need to figure out the bit in the middle. Now save it and if I havent made any typos, your dex and spd stats should be calculated automatically. It's a shame you calculate scores from points, and not the other way around. I have a pretty complex sheet worker posted months ago on the forum that calculates all the hero system stats, but it does it in the reverse order (stat -> cost). That said, working from cost -> stat might simplify that sheet worker. I might dig it up and see how hard it is to reverse the order.
Thanks GiGs...I inserted the sheetworkers and they work perfectly.  I had to make a slight change to the calculation for SPD but everything is working exactly as it should. Now I'm examining all the other autocalcs to tidy everything up. Regards Ray