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

[SCRIPT REQUEST] Simple Encumbrance Tracker

1687748140

Edited 1687748365
Hello script wizards! I am trying to write my first script!  Huzzah!  But it is not working...booo! I am basing it on the framework for @Eric D's Automatic Temp HP Management (from  THIS POST   from over  10 years ago) that watches for changes in the weightttotal attribute value to check for Encumbrance.  Eric D's TempHP script looks for changes in the HP bar and checks for the existance of temp_hp, then offsets the HP changes accordingly. Likewise if it sees a value in the hp_temp attribute, it assigns a status marker to reflect it.  It is a slick script that my party uses often (Paladin and Way of the Long Death Monk). What I want it to do is check for changes in the weightttotal attribute and check for existance of encumbrance attribute. If the Encumbrance attribute value is 1, it should apply a status marker. If it is 0, it should remove the status marker. <<EDIT -- HERE IS MY PROBLEM>> The encumbrance attribute is NaN, so it is not detecting it based on the attribute value.  I have tried substituting the Encumbrance attribute for a simple Strength attribute and multipying that times 15 to get the encumbrance threshold, but it seems my coding skills are lacking to pull that off. Since I do not want it to do anything to HP or any bar value, I have removed all references to such in my version of the script, but have otherwise left the framework the same, subbing out the attribute names and syntax to apply to the encumbrance and weight attributes. To my mind it should be a simple change to Eric D's script, but I cannot get it to parse the change in weighttotal attribute value.  What am I doing wrong? Code: /* global TokenMod, ChatSetAttr */ on('ready', () => { // Configuration parameters const EncMarker = 'SpeedDown::3155973'; const Weight = 'weighttotal'; const EncAttributeName = 'encumbrance'; ///////////////////////////////////////////// const clearURL = /images\/4277467\/iQYjFOsYC5JsuOPUCI9RGA\/.*.png/; const unpackSM = (stats) => stats.split(/,/).reduce((m,v) => { let p = v.split(/@/); let n = parseInt(p[1] || '0', 10); if(p[0].length) { m[p[0]] = Math.max(n, m[p[0]] || 0); } return m; },{}); const packSM = (o) => Object.keys(o) .map(k => ('dead' === k || true === o[k] || o[k]<1 || o[k]>9) ? k : `${k}@${parseInt(o[k])}` ).join(','); const checkEnc = (obj) => { let v = parseFloat(obj.get('current')); findObjs({ type: 'graphic', represents: obj.get('characterid') }) .filter( (t) => t.get(lnk) !== '') .filter( (t) => !clearURL.test(t.get('imgsrc') ) ) .forEach((g)=>{ let sm = unpackSM(g.get('statusmarkers')); if(v>0){ sm[EncMarker]=v; } else { delete sm[EncMarker]; } g.set({ statusmarkers: packSM(sm) }); }); }; const assureEncMarkers = () => { let queue = findObjs({ type: 'attribute', name: EncAttributeName }); const burndownQueue = ()=>{ if(queue.length){ let attr = queue.shift(); checkEnc(attr); setTimeout(burndownQueue,0); } }; burndownQueue(); }; const temporalEncCache = {}; const accountForWeightChange = (obj,prev) => { // 1. did weighttotal change and is it a scale const weighttotal = parseInt(obj.get(value),10); let weight = parseInt(obj.get(value),10); const diff = weight-parseFloat(prev[attr]); if( !isNaN(weighttotal) && diff !== 0 ) { let changes = {}; // 2. does it represent a character // 3. does the hp bar represent an attribute const character = getObj('character',obj.get('represents')); if( diff < 0 && character && obj.get(lnk)!=='' ){ // 4. is there encumbrance const encumbrance = findObjs({ type: 'attribute', characterid: character.id, name: EncAttributeName })[0]; if( encumbrance ) { const now = Date.now(); } } let sm = unpackSM(obj.get('statusmarkers')); changes.statusmarkers = packSM(sm); obj.set(changes); } }; const onAttributeChange = (obj) => { if(obj.get('name') === EncAttributeName){ checkEnc(obj); } }; on("change:attribute", onAttributeChange); on("change:token", accountForWeightChange); if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(accountForWeightChange); } if('undefined' !== typeof ChatSetAttr && ChatSetAttr.registerObserver){ ChatSetAttr.registerObserver('change',onAttributeChange); } assureEncMarkers(); });
1687764006
The Aaron
Roll20 Production Team
API Scripter
I'm on my phone, so can't test, but it's likely all the calls to obj.get(value)  In this context, value is a variable you haven't initialized, so will have the special value undefined. When you call .get() with undefined, you'll get back undefined, then parseInt() will tell you it's Not a Number (NaN).  You almost certainly want: obj.get('current') Which will return the value in the 'current' property.
1687805036

Edited 1687805056
The Aaron
Roll20 Production Team
API Scripter
Ok, I dug into this a bit.  I think there is some superfluous code in there, which I've commented out for now.  Here's a version that should do some of what you want, which you can build from: /* global TokenMod, ChatSetAttr */ on('ready', () => { // Configuration parameters const EncMarker = 'SpeedDown::3155973'; // const Weight = 'weighttotal'; const EncAttributeName = 'encumberance'; // Attribute is misspelled on the 5e sheet... ///////////////////////////////////////////// const clearURL = /images\/4277467\/iQYjFOsYC5JsuOPUCI9RGA\/.*.png/; const unpackSM = (stats) => stats.split(/,/).reduce((m,v) => { let p = v.split(/@/); let n = parseInt(p[1] || '0', 10); if(p[0].length) { m[p[0]] = Math.max(n, m[p[0]] || 0); } return m; },{}); const packSM = (o) => Object.keys(o) .map(k => ('dead' === k || true === o[k] || o[k]<1 || o[k]>9) ? k : `${k}@${parseInt(o[k])}` ).join(','); const checkEnc = (obj) => { let overEnc = obj.get('current').trim().length > 0; findObjs({ type: 'graphic', represents: obj.get('characterid') }) .filter( (t) => !clearURL.test(t.get('imgsrc') ) ) // Ignore BUMP tokens .forEach((g)=>{ let sm = unpackSM(g.get('statusmarkers')); if(overEnc){ sm[EncMarker]=true; // } else { delete sm[EncMarker]; } g.set({ statusmarkers: packSM(sm) }); }); }; const assureEncMarkers = () => { let queue = findObjs({ type: 'attribute', name: EncAttributeName }); const burndownQueue = ()=>{ if(queue.length){ let attr = queue.shift(); checkEnc(attr); setTimeout(burndownQueue,0); } }; burndownQueue(); }; /* // const temporalEncCache = {}; const accountForWeightChange = (obj,prev) => { // 1. did weighttotal change and is it a scale const weighttotal = parseInt(obj.get('current'),10); let weight = parseInt(obj.get('current'),10); const diff = weight-parseFloat(prev[attr]); if( !isNaN(weighttotal) && diff !== 0 ) { let changes = {}; // 2. does it represent a character // 3. does the hp bar represent an attribute const character = getObj('character',obj.get('represents')); if( diff < 0 && character && obj.get(lnk)!=='' ){ // 4. is there encumbrance const encumbrance = findObjs({ type: 'attribute', characterid: character.id, name: EncAttributeName })[0]; if( encumbrance ) { // const now = Date.now(); } } let sm = unpackSM(obj.get('statusmarkers')); changes.statusmarkers = packSM(sm); obj.set(changes); } }; */ const onAttributeChange = (obj) => { if(obj.get('name') === EncAttributeName){ checkEnc(obj); } }; on("change:attribute", onAttributeChange); // on("change:token", accountForWeightChange); /* if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(accountForWeightChange); } */ if('undefined' !== typeof ChatSetAttr && ChatSetAttr.registerObserver){ ChatSetAttr.registerObserver('change',onAttributeChange); } assureEncMarkers(); }); Things of note: The attribute is misspelled on the character sheet as "encumberance", so you have to match that. The value is either empty (technically " ") or "OVER CARRYING CAPACITY", so I changed the check to if the trimmed length is greater than 0.  That means there's no value to use to determine a number (and numbers can only be 0–9, so not sure what you'd have shown). There isn't an feature of the token that has to do with encumbrance, so I commented out the parts that were doing things when tokens changed.  Likely, anything you want to do, you could do based on changes to the character attributes.  Though you might want to force the marker to be added back onto tokens if someone takes it off.
As usual, theAaron,  you rock! I will test this out tonight and start building from there. Clearly I am still a floundering fiundling when it comes to Java. I really appreciate you!
1687891080
The Aaron
Roll20 Production Team
API Scripter
Lol.&nbsp; Java and JavaScript are two completely different languages, so be sure you're explicit if you go searching for things.&nbsp; I suggest finding a copy of Javascript: The Good Parts by Douglas Crockford.&nbsp; I think it's a great place to start from.&nbsp; I don't follow everything he suggests, but you won't go wrong by beginning there.&nbsp; Here are some links to forum threads about Javascript and the API: <a href="https://app.roll20.net/forum/post/6605115/namespaces-novice-seeks-help-exploring-the-revealing-module-pattern" rel="nofollow">https://app.roll20.net/forum/post/6605115/namespaces-novice-seeks-help-exploring-the-revealing-module-pattern</a> <a href="https://app.roll20.net/forum/post/6584105/creating-an-object-that-holds-specific-character-dot-id-and-character-name/?pagenum=1" rel="nofollow">https://app.roll20.net/forum/post/6584105/creating-an-object-that-holds-specific-character-dot-id-and-character-name/?pagenum=1</a> <a href="https://app.roll20.net/forum/post/6237754/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/6237754/slug%7D</a>
Very good stuff, Aaron. Thanks again. I have tested the script out, and it does as I intended.&nbsp; However, I would also like to have the status marker trigger a Combat Master event like TokenMod does. Is there an easy way to register Combat Master as an observer?&nbsp;&nbsp;
1688004686
The Aaron
Roll20 Production Team
API Scripter
The best thing might be to let CombatMaster manage the token.&nbsp; I haven't tested this, but it should use CombatMaster to set the encumbered condition and clear it, but do it directly if CombatMaster isn't installed.&nbsp; YMMV.&nbsp; (Bolded additions/changes) /* global TokenMod, ChatSetAttr, CombatMaster */ on('ready', () =&gt; { // Configuration parameters const EncMarker = 'SpeedDown::3155973'; // const Weight = 'weighttotal'; const EncAttributeName = 'encumberance'; // Attribute is misspelled on the 5e sheet... ///////////////////////////////////////////// const clearURL = /images\/4277467\/iQYjFOsYC5JsuOPUCI9RGA\/.*.png/; const unpackSM = (stats) =&gt; stats.split(/,/).reduce((m,v) =&gt; { let p = v.split(/@/); let n = parseInt(p[1] || '0', 10); if(p[0].length) { m[p[0]] = Math.max(n, m[p[0]] || 0); } return m; },{}); const packSM = (o) =&gt; Object.keys(o) .map(k =&gt; ('dead' === k || true === o[k] || o[k]&lt;1 || o[k]&gt;9) ? k : `${k}@${parseInt(o[k])}` ).join(','); let setCondition; let clearCondition; if('undefined' !== typeof CombatMaster) { setCondition = (token, cond) =&gt; CombatMaster.addConditionToToken(token,cond,-1,"Encumbered"); clearCondition = (token, cond) =&gt; CombatMaster.removeConditionFromToken(token,cond,true); } else { setCondition = (token, cond) =&gt; { let sm = unpackSM(token.get('statusmarkers')); sm[cond]=true; token.set({ statusmarkers: packSM(sm) }); }; clearCondition = (token, cond) =&gt; { let sm = unpackSM(token.get('statusmarkers')); delete sm[cond]; token.set({ statusmarkers: packSM(sm) }); }; } const checkEnc = (obj) =&gt; { let overEnc = obj.get('current').trim().length &gt; 0; findObjs({ type: 'graphic', represents: obj.get('characterid') }) .filter( (t) =&gt; !clearURL.test(t.get('imgsrc') ) ) // Ignore BUMP tokens .forEach((g)=&gt;{ if(overEnc){ setCondition(g,EncMarker); } else { clearCondition(g,EncMarker); } }); }; const assureEncMarkers = () =&gt; { let queue = findObjs({ type: 'attribute', name: EncAttributeName }); const burndownQueue = ()=&gt;{ if(queue.length){ let attr = queue.shift(); checkEnc(attr); setTimeout(burndownQueue,0); } }; burndownQueue(); }; /* // const temporalEncCache = {}; const accountForWeightChange = (obj,prev) =&gt; { // 1. did weighttotal change and is it a scale const weighttotal = parseInt(obj.get('current'),10); let weight = parseInt(obj.get('current'),10); const diff = weight-parseFloat(prev[attr]); if( !isNaN(weighttotal) &amp;&amp; diff !== 0 ) { let changes = {}; // 2. does it represent a character // 3. does the hp bar represent an attribute const character = getObj('character',obj.get('represents')); if( diff &lt; 0 &amp;&amp; character &amp;&amp; obj.get(lnk)!=='' ){ // 4. is there encumbrance const encumbrance = findObjs({ type: 'attribute', characterid: character.id, name: EncAttributeName })[0]; if( encumbrance ) { // const now = Date.now(); } } let sm = unpackSM(obj.get('statusmarkers')); changes.statusmarkers = packSM(sm); obj.set(changes); } }; */ const onAttributeChange = (obj) =&gt; { if(obj.get('name') === EncAttributeName){ checkEnc(obj); } }; on("change:attribute", onAttributeChange); // on("change:token", accountForWeightChange); /* if('undefined' !== typeof TokenMod &amp;&amp; TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(accountForWeightChange); } */ if('undefined' !== typeof ChatSetAttr &amp;&amp; ChatSetAttr.registerObserver){ ChatSetAttr.registerObserver('change',onAttributeChange); } assureEncMarkers(); });
Hrm...&nbsp; Does not seem to work...
1688013351
The Aaron
Roll20 Production Team
API Scripter
Hmm. I'll have to get CombatMaster set up and see what I can do.&nbsp;
@TheAaron, had any luck with this?