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

Request: Script to Modify Token's Attributes by Relative Value

1421592068

Edited 1421592968
I've been looking around the forum for a script, but nothing I've found has the flexibility I need. Pretty much what I want is something like this except that I want to be able to put a formula in the command to create relative and derived values. For example: !attrib Temp HP|@{selected|Temp HP}-5 or !attrib HP|Max|@{selected|Con}*3 (obviously the additional "|" would cause an error in the above script) or !attrib HP|@{selected|HP|Max}/2 and so forth. The referenced script places unusual text such as "$[[0]]" in attribute bars when I try to get too fancy. Can this be done? Is there an existing script that does this? Is it simple enough to write that someone could post a solution here?
1421598743

Edited 1421683785
The Aaron
Pro
API Scripter
Change the last function to this and it will expand inline rolls for you: on("chat:message", function(msg_orig) { if (msg_orig.type == "api" && msg_orig.content.indexOf("!attrib ") !== -1) { // TheAaron's Inline Roll Expansion Code var msg = _.clone(msg_orig); if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } //parse the input into two variables, attribute and newValue var selected = msg.selected; var Parameters = msg.content.split("!attrib ")[1]; var attributeName = Parameters.split("|")[0]; var newValue = Parameters.split("|")[1]; if(!selected) { sendChat("", "/desc Select token and try again."); return; //quit if nothing selected }; //loop through selected tokens _.each(selected, function(obj) { var characterObj = getCharacterObj(obj); if (characterObj == false) return; var attributeObjArray = getAttributeObjects(characterObj, attributeName); if (attributeObjArray == false) return; attrib(characterObj,attributeObjArray,newValue); }); }; }); This will allow you to do relative changes like this: !attrib Temp HP|[[@{selected|Temp HP}-5]]
1421598841
Gen Kitty
Forum Champion
Please look over this script and see if it fits your needs: <a href="https://app.roll20.net/forum/post/1257490/script-t" rel="nofollow">https://app.roll20.net/forum/post/1257490/script-t</a>...
Aaron, could I use this to modify Max values? Does the additional "|" disrupt the command?
1421619984

Edited 1421620628
Also, it doesn't seem to be working now that I've added the new part. I tried again and got this: 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 'type' of undefined at evalmachine.&lt;anonymous&gt;:1312:12 at eval ( Not sure what changed. By "the last function" do you mean line 80 and down? If not, what am I doing wrong?
1421624848
The Aaron
Pro
API Scripter
Yeah, the 80 down. Hmm, guess I'll have to put it in the API and test it.
I did 80 down. totally not working. Let me know what you find.
1421683769
The Aaron
Pro
API Scripter
Ok. Typo by me, here's the whole corrected script. I'll update my above code snippet to fix the type typo: function getAttributeObjects(characterObj,attributeArray) { // can pass array of attribute strings or a single attribute string along with an associated character // returns those attributes as an object array or returns false if they do not exist on the passed character. // get the passed attribute name array from the character object and test if they are defined if (characterObj != undefined ) { var attributeObjArray = new Array(); if (!(attributeArray instanceof Array)) { attributeArray = attributeArray.split(); }; for (var i = 0; i &lt; attributeArray.length; i++) { attributeObjArray[i] = findObjs({_type: "attribute", name: attributeArray[i], _characterid: characterObj.id})[0]; if (attributeObjArray[i] === undefined) { sendChat("API","Selected character requires attribute: " + attributeArray[i] + " "); }; }; }; if (attributeObjArray.indexOf(undefined) !== -1) return false; //loop through attributeArray and names of attributes to make sure they all match and get their values if they are valid. //make sure none of the values are empty var attributeValue = new Array(); var j = 0; for (var i = 0; i &lt; attributeArray.length; i++) { attributeValue[i] = attributeObjArray[i].get("current"); if (attributeValue[i] === "") { sendChat("API"," " + attributeArray[i] + " is empty."); j++; }; }; if (j !== 0) return false; return attributeObjArray; }; //--------------------------------------------------------------------------------------------------------------------------------------------- function getCharacterObj(obj) { //send any object and returns the associated character object //returns character object for attribute, token/graphic, and ability, and... character var objType = obj._type; if ((objType != "attribute") && (objType != "graphic") && (objType != "character")) { sendChat("API"," cannot be associated with a character."); return false; } if ((objType === "attribute") || (objType === "ability")) { var att = getObj(objType, obj._id); if (att.get("_characterid") != "") { var characterObj = getObj("character", att.get("_characterid")); }; }; if (objType === "graphic") { var tok = getObj("graphic", obj._id); if (tok.get("represents") != "") { var characterObj = getObj("character", tok.get("represents")); } else { sendChat("API"," Selected token does not represent a character."); return false; }; }; if (objType === "character") { var characterObj = getObj("character", obj._id); } return characterObj; }; //--------------------------------------------------------------------------------------------------------------------------------------------- function attrib(characterObj,attributeObjArray,newValue) { var attributeName = attributeObjArray[0].get("name"); var attributeValue = attributeObjArray[0].get("current"); var characterName = characterObj.get("name"); // change character attribute attributeObjArray[0].set("current", newValue); //output sendChat("", "/desc " + characterName + " has changed " + attributeName + " from " + attributeValue + " to " + newValue + "."); }; on("chat:message", function(msg_orig) { if (msg_orig.type == "api" && msg_orig.content.indexOf("!attrib ") !== -1) { // TheAaron's Inline Roll Expansion Code var msg = _.clone(msg_orig); if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } //parse the input into two variables, attribute and newValue var selected = msg.selected; var Parameters = msg.content.split("!attrib ")[1]; var attributeName = Parameters.split("|")[0]; var newValue = Parameters.split("|")[1]; if(!selected) { sendChat("", "/desc Select token and try again."); return; //quit if nothing selected }; //loop through selected tokens _.each(selected, function(obj) { var characterObj = getCharacterObj(obj); if (characterObj == false) return; var attributeObjArray = getAttributeObjects(characterObj, attributeName); if (attributeObjArray == false) return; attrib(characterObj,attributeObjArray,newValue); }); }; });
1421685420

Edited 1421685430
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
The Aaron said: Ok. Typo by me, here's the whole corrected script. I'll update my above code snippet to fix the type typo: Saved for posterity's sake.
1421686948

Edited 1421686958
Thank you. It's working fine, except one thing. Let's say that I use !attrib above to change HP Max from 50 to 52 and then immediately try to change HP to max in the same macro function. the output will change the max value only and not the dependent HP value. If I were to make the API wait a few milliseconds, would it solve this problem? Is there a script I could run between two instances of the above script to make it wait, process, and then output?
1421692946
The Aaron
Pro
API Scripter
The issue is that the attribute is expanded during command input, but used during API processing. For example, if you have a critter with 12 hp, and you run this command: !attrib hp_max|[[ @{selected|hp_max} + 2d6]] !attrib hp|@{selected|hp_max} When it gets to the API, it will look (roughly) like this: !attrib hp_max|[[ 12 + 8 ]] !attrib hp|12 This is because the @{selected|hp_max} is expanded to the current value at the time the command is input, but the calculation and setting of the attribute happens while the API is processing the commands. There are some workarounds to this, but you can't really do them with !attrib directly. If your attribute is being referenced by one of the selected token's bars, you can use tokenmod to handle this: !token-mod --set bar1|[[@{selected|bar1_max}+2d6]] This works because bar1 for TokenMod is a pseudo property that is expanded to set both bar1_current and bar1_max on the API side.
1421692979
The Aaron
Pro
API Scripter
Stephen S. said: The Aaron said: Ok. Typo by me, here's the whole corrected script. I'll update my above code snippet to fix the type typo: Saved for posterity's sake. Ha!
And if it's not linked to a bar, is there still a solution?
1421701850
The Aaron
Pro
API Scripter
Yeah, but it's as yet unwritten. =D I've been meaning to write a CharMod that works like TokenMod, but for characters. As you know though, I have a hard time finding time to write scripts in. I'm hoping 2015 will be the year of the API Script Explosion, in a good way. =D Certainly there are a bunch of things coming down the pipe that will be awesome for the API. =D
I'm hoping so. That would be pretty great.
How do you modify Max values? I could automatically stat up many characters if I were able to define !attrib HP|Max. Alas, it does not seem possible with the script in question.
1421770574
The Aaron
Pro
API Scripter
You are correct, it is not possible with that script.
I have a request/problem. When using this script with @{target} anything, the act of clicking something new unselects the current token, thereby invalidating the function. Anything that utilizes both targets and this script fails. For example, if you were to cast a spell that, say, chose a target but cost the user 2 Mana Points (from the character's mana attribute), it pretty much wouldn't work at all. Is there a way to work around this?
1422158266
The Aaron
Pro
API Scripter
I don't really want to be in the business of maintaining someone else's scripts... =D But since it seems I am, try this: function getAttributeObjects(characterObj,attributeArray) { // can pass array of attribute strings or a single attribute string along with an associated character // returns those attributes as an object array or returns false if they do not exist on the passed character. // get the passed attribute name array from the character object and test if they are defined if (characterObj != undefined ) { var attributeObjArray = new Array(); if (!(attributeArray instanceof Array)) { attributeArray = attributeArray.split(); }; for (var i = 0; i &lt; attributeArray.length; i++) { attributeObjArray[i] = findObjs({_type: "attribute", name: attributeArray[i], _characterid: characterObj.id})[0]; if (attributeObjArray[i] === undefined) { sendChat("API","Selected character requires attribute: " + attributeArray[i] + " "); }; }; }; if (attributeObjArray.indexOf(undefined) !== -1) return false; //loop through attributeArray and names of attributes to make sure they all match and get their values if they are valid. //make sure none of the values are empty var attributeValue = new Array(); var j = 0; for (var i = 0; i &lt; attributeArray.length; i++) { attributeValue[i] = attributeObjArray[i].get("current"); if (attributeValue[i] === "") { sendChat("API"," " + attributeArray[i] + " is empty."); j++; }; }; if (j !== 0) return false; return attributeObjArray; }; //--------------------------------------------------------------------------------------------------------------------------------------------- function getCharacterObj(obj) { //send any object and returns the associated character object //returns character object for attribute, token/graphic, and ability, and... character var objType = obj._type; if ((objType != "attribute") && (objType != "graphic") && (objType != "character")) { sendChat("API"," cannot be associated with a character."); return false; } if ((objType === "attribute") || (objType === "ability")) { var att = getObj(objType, obj._id); if (att.get("_characterid") != "") { var characterObj = getObj("character", att.get("_characterid")); }; }; if (objType === "graphic") { var tok = getObj("graphic", obj._id); if (tok.get("represents") != "") { var characterObj = getObj("character", tok.get("represents")); } else { sendChat("API"," Selected token does not represent a character."); return false; }; }; if (objType === "character") { var characterObj = getObj("character", obj._id); } return characterObj; }; //--------------------------------------------------------------------------------------------------------------------------------------------- function attrib(characterObj,attributeObjArray,newValue) { var attributeName = attributeObjArray[0].get("name"); var attributeValue = attributeObjArray[0].get("current"); var characterName = characterObj.get("name"); // change character attribute attributeObjArray[0].set("current", newValue); //output sendChat("", "/desc " + characterName + " has changed " + attributeName + " from " + attributeValue + " to " + newValue + "."); }; on("chat:message", function(msg_orig) { if (msg_orig.type == "api" && msg_orig.content.indexOf("!attrib ") !== -1) { // TheAaron's Inline Roll Expansion Code var msg = _.clone(msg_orig); if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } //parse the input into two variables, attribute and newValue var selected = msg.selected; var args = msg.content.split(/\s+/); var attributeName = args[1].split("|")[0]; var newValue = args[1].split("|")[1]; // Add selected as IDs following other parameters if(!selected) { selected = _.chain(args) .rest(2) .uniq() .map(function(t){ return { _type: 'graphic', _id: t }; }) .value(); } if(!selected) { sendChat("", "/desc Select token and try again."); return; //quit if nothing selected }; //loop through selected tokens _.each(selected, function(obj) { var characterObj = getCharacterObj(obj); if (characterObj == false) return; var attributeObjArray = getAttributeObjects(characterObj, attributeName); if (attributeObjArray == false) return; attrib(characterObj,attributeObjArray,newValue); }); }; }); This should let you specify token_ids after the other arguments thusly: !attrib blah|[[@{target|tab}]] @{selected|token_id}
I'm trying it but I don't think I'm understanding it. Here's the gist of what I would have done previously: /em attacks @{target|token_name} with a special attack! !attrib AP}[[@{selected|AP}-1]] I don't know get what part of this should use that argument.
1422160681
The Aaron
Pro
API Scripter
/em attacks @{target|token_name} with a special attack! !attrib AP}[[@{selected|AP}-1]] @{selected|token_id} The issue is that when the command is sent to the API, there is nothing selected to build the msg.selected array. I added parsing anything after the initial arguments as token_ids which are used to construct a fake msg.selected. This isn't an ideal solution of course, but it works for 1 token, or if you get the token_ids manually and build a whole list.
1422161636

Edited 1422162505
Thank you very much.
1422165118
The Aaron
Pro
API Scripter
no problem! =D
Is it just me or did the last update remove my ability to modify two-word attributes? When I try to modify a stat like, say, "Hit Points", it tells me there is no such attribute as "Hit
1422234523
The Aaron
Pro
API Scripter
Ah, yes it did. I'll fix that later tonight. :)
Thank you. I'd really appreciate that.
1422296742
The Aaron
Pro
API Scripter
Oops! Maybe tonight instead...
Oh, didn't even notice because I'd been offline for a while. I'll check again tonight.
1422304540

Edited 1423237217
The Aaron
Pro
API Scripter
Oh.. I wrote a followup post to that one and seem to have not sent it... I made a change, you need to have ' -- ' between the arguments and the token ids: /em attacks @{target|token_name} with a special attack! !attrib AP|[[@{selected|AP}-1]] -- @{selected|token_id} Here's the new code: function getAttributeObjects(characterObj,attributeArray) { // can pass array of attribute strings or a single attribute string along with an associated character // returns those attributes as an object array or returns false if they do not exist on the passed character. // get the passed attribute name array from the character object and test if they are defined if (characterObj != undefined ) { var attributeObjArray = new Array(); if (!(attributeArray instanceof Array)) { attributeArray = attributeArray.split(); }; for (var i = 0; i &lt; attributeArray.length; i++) { attributeObjArray[i] = findObjs({_type: "attribute", name: attributeArray[i], _characterid: characterObj.id})[0]; if (attributeObjArray[i] === undefined) { sendChat("API","Selected character requires attribute: " + attributeArray[i] + " "); }; }; }; if (attributeObjArray.indexOf(undefined) !== -1) return false; //loop through attributeArray and names of attributes to make sure they all match and get their values if they are valid. //make sure none of the values are empty var attributeValue = new Array(); var j = 0; for (var i = 0; i &lt; attributeArray.length; i++) { attributeValue[i] = attributeObjArray[i].get("current"); if (attributeValue[i] === "") { sendChat("API"," " + attributeArray[i] + " is empty."); j++; }; }; if (j !== 0) return false; return attributeObjArray; }; //--------------------------------------------------------------------------------------------------------------------------------------------- function getCharacterObj(obj) { //send any object and returns the associated character object //returns character object for attribute, token/graphic, and ability, and... character var objType = obj._type; if ((objType != "attribute") && (objType != "graphic") && (objType != "character")) { sendChat("API"," cannot be associated with a character."); return false; } if ((objType === "attribute") || (objType === "ability")) { var att = getObj(objType, obj._id); if (att.get("_characterid") != "") { var characterObj = getObj("character", att.get("_characterid")); }; }; if (objType === "graphic") { var tok = getObj("graphic", obj._id); if (tok.get("represents") != "") { var characterObj = getObj("character", tok.get("represents")); } else { sendChat("API"," Selected token does not represent a character."); return false; }; }; if (objType === "character") { var characterObj = getObj("character", obj._id); } return characterObj; }; //--------------------------------------------------------------------------------------------------------------------------------------------- function attrib(characterObj,attributeObjArray,newValue) { var attributeName = attributeObjArray[0].get("name"); var attributeValue = attributeObjArray[0].get("current"); var characterName = characterObj.get("name"); // change character attribute attributeObjArray[0].set("current", newValue); //output sendChat("", "/desc " + characterName + " has changed " + attributeName + " from " + attributeValue + " to " + newValue + "."); }; on("chat:message", function(msg_orig) { if (msg_orig.type == "api" && msg_orig.content.indexOf("!attrib ") !== -1) { // TheAaron's Inline Roll Expansion Code var msg = _.clone(msg_orig); if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } //parse the input into two variables, attribute and newValue var selected = msg.selected; var Parameters = msg.content.split(/\s+--\s+/)[0].split("!attrib ")[1]; var ids = msg.content.split(/\s+--\s+/)[1].split(/\s+/); var attributeName = Parameters.split("|")[0]; var newValue = Parameters.split("|")[1]; // Add selected as IDs following other parameters if(!selected) { selected = _.chain(ids) .uniq() .map(function(t){ return { _type: 'graphic', _id: t }; }) .value(); } if(!selected) { sendChat("", "/desc Select token and try again."); return; //quit if nothing selected }; //loop through selected tokens _.each(selected, function(obj) { var characterObj = getCharacterObj(obj); if (characterObj == false) return; var attributeObjArray = getAttributeObjects(characterObj, attributeName); if (attributeObjArray == false) return; attrib(characterObj,attributeObjArray,newValue); }); }; });
All of my attempts to use the new one result in: 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 call method 'split' of undefined at evalmachine.&lt;anonymous&gt;:1329:52 at eval (
1422305407
The Aaron
Pro
API Scripter
Bummer. Ok. I'll have to debug it tonight then. =/
If nothing else, I hope you value my ability to find bugs?
1422335959
The Aaron
Pro
API Scripter
Highly prized. =D This seems to work for my test with multi-word attributes and selected ids: function getAttributeObjects(characterObj,attributeArray) { // can pass array of attribute strings or a single attribute string along with an associated character // returns those attributes as an object array or returns false if they do not exist on the passed character. // get the passed attribute name array from the character object and test if they are defined if (characterObj != undefined ) { var attributeObjArray = new Array(); if (!(attributeArray instanceof Array)) { attributeArray = attributeArray.split(); }; for (var i = 0; i &lt; attributeArray.length; i++) { attributeObjArray[i] = findObjs({_type: "attribute", name: attributeArray[i], _characterid: characterObj.id})[0]; if (attributeObjArray[i] === undefined) { sendChat("API","Selected character requires attribute: " + attributeArray[i] + " "); }; }; }; if (attributeObjArray.indexOf(undefined) !== -1) return false; //loop through attributeArray and names of attributes to make sure they all match and get their values if they are valid. //make sure none of the values are empty var attributeValue = new Array(); var j = 0; for (var i = 0; i &lt; attributeArray.length; i++) { attributeValue[i] = attributeObjArray[i].get("current"); if (attributeValue[i] === "") { sendChat("API"," " + attributeArray[i] + " is empty."); j++; }; }; if (j !== 0) return false; return attributeObjArray; }; //--------------------------------------------------------------------------------------------------------------------------------------------- function getCharacterObj(obj) { //send any object and returns the associated character object //returns character object for attribute, token/graphic, and ability, and... character var objType = obj._type; if ((objType != "attribute") && (objType != "graphic") && (objType != "character")) { sendChat("API"," cannot be associated with a character."); return false; } if ((objType === "attribute") || (objType === "ability")) { var att = getObj(objType, obj._id); if (att.get("_characterid") != "") { var characterObj = getObj("character", att.get("_characterid")); }; }; if (objType === "graphic") { var tok = getObj("graphic", obj._id); if (tok.get("represents") != "") { var characterObj = getObj("character", tok.get("represents")); } else { sendChat("API"," Selected token does not represent a character."); return false; }; }; if (objType === "character") { var characterObj = getObj("character", obj._id); } return characterObj; }; //--------------------------------------------------------------------------------------------------------------------------------------------- function attrib(characterObj,attributeObjArray,newValue) { var attributeName = attributeObjArray[0].get("name"); var attributeValue = attributeObjArray[0].get("current"); var characterName = characterObj.get("name"); // change character attribute attributeObjArray[0].set("current", newValue); //output sendChat("", "/desc " + characterName + " has changed " + attributeName + " from " + attributeValue + " to " + newValue + "."); }; on("chat:message", function(msg_orig) { if (msg_orig.type == "api" && msg_orig.content.indexOf("!attrib ") !== -1) { // TheAaron's Inline Roll Expansion Code var msg = _.clone(msg_orig); if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } //parse the input into two variables, attribute and newValue var selected = msg.selected; var Parameters = msg.content.split(/\s+--\s+/)[0].split("!attrib ")[1]; var ids = (msg.content.split(/\s+--\s+/)[1] ||'').split(/\s+/); var attributeName = Parameters.split("|")[0]; var newValue = Parameters.split("|")[1]; // Add selected as IDs following other parameters if(!selected) { selected = _.chain(ids) .uniq() .filter(function(s){return s.length&gt;0;}) .map(function(t){ return { _type: 'graphic', _id: t }; }) .value(); } if(!selected) { sendChat("", "/desc Select token and try again."); return; //quit if nothing selected }; //loop through selected tokens _.each(selected, function(obj) { var characterObj = getCharacterObj(obj); if (characterObj == false) return; var attributeObjArray = getAttributeObjects(characterObj, attributeName); if (attributeObjArray == false) return; attrib(characterObj,attributeObjArray,newValue); }); }; });
Thank you again. I can not even express how helpful this is!
1422375467
The Aaron
Pro
API Scripter
=D No worries.
How do I whisper the change report to the selected character so that only the user knows the new value?
1422861472
The Aaron
Pro
API Scripter
Change /desc to /w in the function attrib will mostly work. I can get you a better hack tomorrow. (I just crawled into bed!! =D )
When I whisper to @{selected|token_name}, the output console says: "ERROR: Unable to find character selected in chat command." {"who":"error","type":"error","content":"Unable to find a player or character with name: selected|token_nameleader"} But I'm not in a hurry, so get some sleep. Thank you.
Did you happen to figure it out?
You can't whisper to a token.
I know, but whispering to the character doesn't help either: {"who":"error","type":"error","content":"Unable to find a player or character with name: selected|character_name"} "ERROR: Unable to find character selected in chat command."
1423235151

Edited 1423235176
Change it to: sendChat("", "/w " + msg.who + " + characterName + " has changed " + attributeName + " from " + attributeValue + " to " + newValue + "."); You might also want to double that line and do a /w GM too.
1423237634

Edited 1423237710
The Aaron
Pro
API Scripter
Added a !wattrib that whispers to the character instead of saying it as a description: function getAttributeObjects(characterObj,attributeArray) { "use strict"; var i, j = 0, attributeObjArray = [], attributeValue = [] ; // can pass array of attribute strings or a single attribute string along with an associated character // returns those attributes as an object array or returns false if they do not exist on the passed character. // get the passed attribute name array from the character object and test if they are defined if (characterObj) { if (!(attributeArray instanceof Array)) { attributeArray = attributeArray.split(); } for (i = 0; i &lt; attributeArray.length; i++) { attributeObjArray[i] = findObjs({_type: "attribute", name: attributeArray[i], _characterid: characterObj.id})[0]; if (attributeObjArray[i] === undefined) { sendChat("API","Selected character requires attribute: " + attributeArray[i] + " "); } } } if (attributeObjArray.indexOf(undefined) !== -1) { return false; } //loop through attributeArray and names of attributes to make sure they all match and get their values if they are valid. //make sure none of the values are empty for (i = 0; i &lt; attributeArray.length; i++) { attributeValue[i] = attributeObjArray[i].get("current"); if (attributeValue[i] === "") { sendChat("API"," " + attributeArray[i] + " is empty."); j++; } } if (j !== 0) { return false; } return attributeObjArray; } //--------------------------------------------------------------------------------------------------------------------------------------------- function getCharacterObj(obj) { "use strict"; //send any object and returns the associated character object //returns character object for attribute, token/graphic, and ability, and... character var objType = obj._type, att, characterObj, tok; if ((objType !== "attribute") && (objType !== "graphic") && (objType !== "character")) { sendChat("API"," cannot be associated with a character."); return false; } if ((objType === "attribute") || (objType === "ability")) { att = getObj(objType, obj._id); if (att.get("_characterid") !== "") { characterObj = getObj("character", att.get("_characterid")); } } if (objType === "graphic") { tok = getObj("graphic", obj._id); if (tok.get("represents") !== "") { characterObj = getObj("character", tok.get("represents")); } else { sendChat("API"," Selected token does not represent a character."); return false; } } if (objType === "character") { characterObj = getObj("character", obj._id); } return characterObj; } //--------------------------------------------------------------------------------------------------------------------------------------------- function attrib(characterObj,attributeObjArray,newValue,isWhisper) { "use strict"; var attributeName = attributeObjArray[0].get("name"), attributeValue = attributeObjArray[0].get("current"), characterName = characterObj.get("name"); // change character attribute attributeObjArray[0].set("current", newValue); //output sendChat("", ( isWhisper ? ("/w "+characterName.split(/\s+/)[0] +" ") : "/desc ") + characterName + " has changed " + attributeName + " from " + attributeValue + " to " + newValue + "."); } on("chat:message", function(msg_orig) { "use strict"; var msg, selected, isWhisper, Parameters, ids, attributeName, newValue, characterObj, attributeObjArray; if (msg_orig.type === "api" && ( msg_orig.content.indexOf("!attrib ") !== -1 || msg_orig.content.indexOf("!wattrib ") !== -1 ) ){ // TheAaron's Inline Roll Expansion Code msg = _.clone(msg_orig); if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } //parse the input into two variables, attribute and newValue selected = msg.selected; isWhisper = !!msg.content.match(/^!w/); Parameters = msg.content.split(/\s+--\s+/)[0].split(/![w]?attrib /)[1]; ids = (msg.content.split(/\s+--\s+/)[1] ||'').split(/\s+/); attributeName = Parameters.split("|")[0]; newValue = Parameters.split("|")[1]; // Add selected as IDs following other parameters if(!selected) { selected = _.chain(ids) .uniq() .filter(function(s){return s.length&gt;0;}) .map(function(t){ return { _type: 'graphic', _id: t }; }) .value(); } if(!selected) { sendChat("", "/desc Select token and try again."); return; //quit if nothing selected } //loop through selected tokens _.each(selected, function(obj) { characterObj = getCharacterObj(obj); if ( ! characterObj) { return; } attributeObjArray = getAttributeObjects(characterObj, attributeName); if ( ! attributeObjArray) { return; } attrib(characterObj,attributeObjArray,newValue,isWhisper); }); } });