[Help] 5e Script - Inspiration tracking

1506480111

Edited 1506606172
Giger
Pro
API Scripter
Howdy all, i'm hoping someone can help me here. I'm tired of players not realizing or forgetting they have inspiration, and having to check to see who, if anyone, has it. I looked around and couldn't find any APIs that would do anything like this, so i thought i'd give it a try  - turns out i'm not terribly good :P Determining if the right attribute is changed, to the right value was easy enough. Now i need to do more fancy things like, get the character's name, and figure out how to update a token with a status effect. I have a good idea how to approach all of that, but I for the life of me, can't seem to reach out to other objects, despite being able to pass around the ID, i pulled from the attribute obj. Help with pulling this off would be fantastic, although if anyone knows a script that would just do this, that also be great on("change:attribute", function(obj, prev) {     //Only do stuff is the inspiration attribute was changed.     var isupdated = obj.get("name");     var attr = obj.get("current");          if (isupdated =="inspiration" && attr=="on"){                 var id = obj.get("_characterid");                  var char = findObjs({_type: "character",_characterid: id})[0];         var cname = char["name"];          sendChat ("", cname+"");              }           });
1506483288
The Aaron
Pro
API Scripter
In general, I find it better not to preface with _ on property names (with the exception of registering for events on them, which requires the _). It looks like you're going along well until you get to the cname line.  (Note: "char" is technically a reserved name in Javascript, so using "character" instead would be better.).  The issue is likely this line: var cname = char["name"]; You can't index Roll20 Character Objects with array notation, you need to use the .get() method: var cname = char.get("name"); That should fix it.
1506483805
I use TheAaron's TokenMod to help players remember Inspiration (and other buffs like Rage), adding one of the token status markers to display it on the tabletop.  This is what mine looks like: !token-mod {{   --set statusmarkers|!chained-heart   --ids     @{target|Bardic Inspiration|token_id} } When the bard casts the buff, he fires the macro and selects the character token he wants to buff. The API adds the status marker. I have another macro to roll the bardic inspiration die, and it removes the status marker. Note: you'll need to toggle on the "players can use on tokens they don't control" feature in TokenMod.
This isn't directly helpful to the API script in question, but on the subject of "how I handle inspiration in my game," I use this card I made in Photoshop as a 1-card, infinite-size deck in Roll20 and just drag cards onto players' portraits when they receive inspiration. That the card appears above their portrait is a nice visual cue to everyone.
1506518834
Giger
Pro
API Scripter
The Aaron said: In general, I find it better not to preface with _ on property names (with the exception of registering for events on them, which requires the _). It looks like you're going along well until you get to the cname line.  (Note: "char" is technically a reserved name in Javascript, so using "character" instead would be better.).  The issue is likely this line: var cname = char["name"]; You can't index Roll20 Character Objects with array notation, you need to use the .get() method: var cname = char.get("name"); That should fix it. Thanks Aaron!  and everyone else for their suggestions. I tried the token mod, but my macro button bar is pretty full already so i'm starting to have to find new ways to do things. If i can ever get it working ill be sure to share it.
1506524499

Edited 1506524997
Giger
Pro
API Scripter
So i got everything "working" until i need to find the characters token, so i can change the status icon. I'm pressuming i need to identify the "default token" of the character sheet, and then use that ID moving forward to update it with a new icon.      var Character = getObj("character", id);         var Token = Character.get["_defaulttoken"];         log (Character); The Character var is working, I think need to parse the string or something LOG {"name":"As Himself","bio":1505620692225,"gmnotes":"","_defaulttoken":1503974671085,"archived":false,"inplayerjournals":"all","controlledby":"-KtWQIVGMmFxlA0o7me0","_id":"-KsOpnYOYha1N3wLILze","_type":"character","avatar":"redacted"} Using v ar Token = Character.get("_defaulttoken"); generates the following error undefined
1506525073
The Aaron
Pro
API Scripter
Actually, it's easier to just find all the tokens that represent that character: const getCharacterTokens = (cid) => findObjs({type:'graphic'}).filter((t)=>t.get('represents')===cid); /* ... */ getCharacterTokens(character.id).forEach((t)=>t.set('status_skull',true));
1506526463
Giger
Pro
API Scripter
You're my hero, and brilliant! All i can offer is my Patronage in return!
1506527044
The Aaron
Pro
API Scripter
HAHAHA, No need or worry. =D  I'm happy to help.
1506535448

Edited 1506536190
Giger
Pro
API Scripter
Here's the proposed final solution.  Credits to TheAaron for all the help, and Sky (as i stole some of his pretty css wizardry.[pending approval]) What am I missing? What are some considerations for code complete. AnnounceInspiration = function (id) {           //character_ID is passed from on change           //sendChat("",id);           if ( _.isEmpty(id)) return;            var character = getObj("character", id);               log (character);                                       const getCharacterTokens = (cid) => findObjs({type:'graphic'}).filter((t)=>t.get('represents')===cid);                       /* ... */         getCharacterTokens(character.id).forEach((t)=>t.set('status_black-flag',true));         var AlertTokenName = character.get("name");                               var AlertColor = getObj("player", character.get("controlledby").split(",")[0]).get("color");         var AlertTextColor = (getBrightness(AlertColor) < (255 / 2)) ? "#FFF" : "#000";         var AlertShadowColor = (AlertTextColor == "#000") ? "#FFF" : "#000";         var AlertOuterStyle = "max-height: 40px; width: 100%; margin: 10px 0px 5px -7px; line-height: 40px;";         var AlertInnerStyle = "max-height: 20px; width: 100%; margin: 0px; padding: 0px 0px 2px 0px; clear: both; overflow: hidden; font-family: Candal; font-weight: lighter; font-size: 13px; line-height: 20px; color: " + AlertTextColor + "; background-color: " + AlertColor + "; background-image: linear-gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); border: 1px solid #000; border-radius: 4px; text-shadow: -1px -1px 0 " + AlertShadowColor + ", 1px -1px 0 " + AlertShadowColor + ", -1px 1px 0 " + AlertShadowColor + ", 1px 1px 0 " + AlertShadowColor + ";";         var AlertImageStyle = "height: 40px; width: 40px; float: right; margin: -32px 5px 0px 0px;";         sendChat("", "/desc <div style='" + AlertOuterStyle + "'><div style='" + AlertInnerStyle + "'>" + AlertTokenName + " is Inspired! </div><img src='https://s3.amazonaws.com/files.d20.io/images/39783029/-w45_4ICV9QnFzijBimwKA/max.png' style='" + AlertImageStyle + "'></img></div>");         };         DropInspiration = function (id) {           //character_ID is passed from on change           //sendChat("",id);           if ( _.isEmpty(id)) return;            var character = getObj("character", id);             //  log ('dropped');                                       const getCharacterTokens = (cid) => findObjs({type:'graphic'}).filter((t)=>t.get('represents')===cid);                       /* ... */                    getCharacterTokens(character.id).forEach((t)=>t.set('status_black-flag',false));                        };          on("ready", function(){log('-=> HazInspiration <=- V0.0.1');           sendChat("", "/desc <div style='max-height: 40px; width: 100%; margin: 10px 0px 5px -7px; line-height: 40px;'><div style='max-height: 20px; width: 100%; margin: 0px; padding: 0px 0px 2px 0px; clear: both; overflow: hidden; font-family: Candal; font-weight: lighter; font-size: 13px; line-height: 20px; color: fff; background-color: 20b2aa; background-image: linear-gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); border: 1px solid #000; border-radius: 4px; text-shadow: -1px -1px 0 000, 1px -1px 0 000, -1px 1px 0 000, 1px 1px 0 000'>Inspiration Tracking Online!</div></div>");      });                 on("chat:message", function(msg) {       if(msg.type == "api" && msg.content.indexOf("!inspiration") !== -1) {            sendChat("", "/desc <div style='max-height: 40px; width: 100%; margin: 10px 0px 5px -7px; line-height: 40px;'><div style='max-height: 20px; width: 100%; margin: 0px; padding: 0px 0px 2px 0px; clear: both; overflow: hidden; font-family: Candal; font-weight: lighter; font-size: 13px; line-height: 20px; color: fff; background-color: 20b2aa; background-image: linear-gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); border: 1px solid #000; border-radius: 4px; text-shadow: -1px -1px 0 000, 1px -1px 0 000, -1px 1px 0 000, 1px 1px 0 000'>Inspiration Tracking Online!</div></div>");      }       });              // Has a character sheet been updated?       on("change:attribute", function(obj, prev) {          var isupdated = obj.get("name");           if (isupdated =="inspiration"){               // Only work if 'inspiration' was changed               var val = obj.get("current");               var id = obj.get("_characterid");                 //Inspired-  Now, decide, is it turned on or off?                   if (isupdated =="inspiration" && val == "on"){                    //  It's On'                      var charname = getAttrByName(id, "character_name");                      //Get the Character Name for Annoucing                           if(charname != ""){                             //A Name was found, let's move on.                             AnnounceInspiration(id);                           } else {sendChat("", "Failed getting Character Name") ;                                     //Couldn't find a name                                  }                                            } else if (isupdated =="inspiration" && val != "on"){                         //It's Off                         DropInspiration(id);                                        }                      // Silent if not inspired                                }              });                     
1506537670
The Aaron
Pro
API Scripter
Sky won't mind if you use his functions and he's unlikely to respond to messages, but I'll ask him for you. You'll need to pull over his getBrightness function.  It's a good idea to wrap your things either explicitly in an object or implicitly by doing them all in an on('ready') function.  That prevents polluting the global scope and prevents anything from slipping into your code unintentionally.  It's also a good idea (for historic reasons) to start functions with a lowercase letter, though that's probably going away.  Also, no need to redeclare getCharacterTokens(), you can move it out to a first class citizen of your script. =D Anyway, something like this: on('ready',()=>{ const getCharacterTokens = (cid) => findObjs({type:'graphic'}).filter((t)=>t.get('represents')===cid); /* copy this... from wherever... const getBrightness = ()=>0; const announceInspiration = function (id) { //character_ID is passed from on change //sendChat("",id); if ( _.isEmpty(id)) return; var character = getObj("character", id); getCharacterTokens(character.id).forEach((t)=>t.set('status_black-flag',true)); var AlertTokenName = character.get("name"); var AlertColor = getObj("player", character.get("controlledby").split(",")[0]).get("color"); var AlertTextColor = (getBrightness(AlertColor) < (255 / 2)) ? "#FFF" : "#000"; var AlertShadowColor = (AlertTextColor == "#000") ? "#FFF" : "#000"; var AlertOuterStyle = "max-height: 40px; width: 100%; margin: 10px 0px 5px -7px; line-height: 40px;"; var AlertInnerStyle = "max-height: 20px; width: 100%; margin: 0px; padding: 0px 0px 2px 0px; clear: both; overflow: hidden; font-family: Candal; font-weight: lighter; font-size: 13px; line-height: 20px; color: " + AlertTextColor + "; background-color: " + AlertColor + "; background-image: linear-gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); border: 1px solid #000; border-radius: 4px; text-shadow: -1px -1px 0 " + AlertShadowColor + ", 1px -1px 0 " + AlertShadowColor + ", -1px 1px 0 " + AlertShadowColor + ", 1px 1px 0 " + AlertShadowColor + ";"; var AlertImageStyle = "height: 40px; width: 40px; float: right; margin: -32px 5px 0px 0px;"; sendChat("", "/desc <div style='" + AlertOuterStyle + "'><div style='" + AlertInnerStyle + "'>" + AlertTokenName + " is Inspired! </div><img src='https://s3.amazonaws.com/files.d20.io/images/39783029/-w45_4ICV9QnFzijBimwKA/max.png' style='" + AlertImageStyle + "'></img></div>"); }; const dropInspiration = function (id) { //character_ID is passed from on change if ( _.isEmpty(id)) return; var character = getObj("character", id); getCharacterTokens(character.id).forEach((t)=>t.set('status_black-flag',false)); }; on("chat:message", function(msg) { if(msg.type == "api" && msg.content.indexOf("!inspiration") !== -1) {  sendChat("", "/desc <div style='max-height: 40px; width: 100%; margin: 10px 0px 5px -7px; line-height: 40px;'><div style='max-height: 20px; width: 100%; margin: 0px; padding: 0px 0px 2px 0px; clear: both; overflow: hidden; font-family: Candal; font-weight: lighter; font-size: 13px; line-height: 20px; color: fff; background-color: 20b2aa; background-image: linear-gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); border: 1px solid #000; border-radius: 4px; text-shadow: -1px -1px 0 000, 1px -1px 0 000, -1px 1px 0 000, 1px 1px 0 000'>Inspiration Tracking Online!</div></div>"); } }); // Has a character sheet been updated? on("change:attribute", function(obj) { var isupdated = obj.get("name"); if (isupdated =="inspiration"){ // Only work if 'inspiration' was changed var val = obj.get("current"); var id = obj.get("_characterid");   //Inspired-  Now, decide, is it turned on or off? if (isupdated =="inspiration" && val == "on"){ //  It's On' var charname = getAttrByName(id, "character_name"); //Get the Character Name for annoucing if(charname != ""){ //A Name was found, let's move on. announceInspiration(id); } else {sendChat("", "Failed getting Character Name") ; //Couldn't find a name } } else if (isupdated =="inspiration" && val != "on"){ //It's Off dropInspiration(id); }  // Silent if not inspired } }); log('-=> HazInspiration <=- V0.0.1');   sendChat("", "/desc <div style='max-height: 40px; width: 100%; margin: 10px 0px 5px -7px; line-height: 40px;'><div style='max-height: 20px; width: 100%; margin: 0px; padding: 0px 0px 2px 0px; clear: both; overflow: hidden; font-family: Candal; font-weight: lighter; font-size: 13px; line-height: 20px; color: fff; background-color: 20b2aa; background-image: linear-gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); border: 1px solid #000; border-radius: 4px; text-shadow: -1px -1px 0 000, 1px -1px 0 000, -1px 1px 0 000, 1px 1px 0 000'>Inspiration Tracking Online!</div></div>"); });
1506558696

Edited 1506559272
Giger
Pro
API Scripter
Thanks again TheAaron, i was able to connect with him earlier this evening and, as you suspected, he was cool with it. I got the missing function added and everything seems to be working well.  Can't really figure out github, so i'll just dump this here for anyone else that wants to use it Only little bug i can find is, if inspiration has never been toggled, the attribute won't yet exist and so the script won't fire the first time.  Works fine afterwards though. //HazInspiration v. 0.2 created  on the 9-27-2017 by Giger, my first script with massive help from TheAaron and code shamelessly stolen from Sky's Initiative System (it's pretty) /*     This script reports to the chat window, and updates the players token with an icon, to indicate whether the player has Inspiration or not.     Designed for the 5th Edition ( OGL by Roll20 ) Character Sheet.      */       on('ready',()=>{       const getCharacterTokens = (cid) => findObjs({type:'graphic'}).filter((t)=>t.get('represents')===cid);       //Brightness            var getBrightness = getBrightness || {};           var getHex2Dec = getHex2Dec || {};                  function getBrightness(hex) {               hex = hex.replace('#', '');               var c_r = getHex2Dec(hex.substr(0, 2));               var c_g = getHex2Dec(hex.substr(2, 2));               var c_b = getHex2Dec(hex.substr(4, 2));               return ((c_r * 299) + (c_g * 587) + (c_b * 114)) / 1000;           };                  function getHex2Dec(hex_string) {               hex_string = (hex_string + '').replace(/[^a-f0-9]/gi, '');               return parseInt(hex_string, 16);           };              const announceInspiration = function (id) {       //Handles Chat Annoucement       if ( _.isEmpty(id)) return;             var character = getObj("character", id);       var AlertTokenName = character.get("name");       var AlertColor = getObj("player", character.get("controlledby").split(",")[0]).get("color");       var AlertTextColor = (getBrightness(AlertColor) < (255 / 2)) ? "#FFF" : "#000";       var AlertShadowColor = (AlertTextColor == "#000") ? "#FFF" : "#000";       var AlertOuterStyle = "max-height: 40px; width: 100%; margin: 10px 0px 5px -7px; line-height: 40px;";       var AlertInnerStyle = "max-height: 20px; width: 100%; margin: 0px; padding: 0px 0px 2px 0px; clear: both; overflow: hidden; font-family: Candal; font-weight: lighter; font-size: 13px; line-height: 20px; color: " + AlertTextColor + "; background-color: " + AlertColor + "; background-image: linear-gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); border: 1px solid #000; border-radius: 4px; text-shadow: -1px -1px 0 " + AlertShadowColor + ", 1px -1px 0 " + AlertShadowColor + ", -1px 1px 0 " + AlertShadowColor + ", 1px 1px 0 " + AlertShadowColor + ";";       var AlertImageStyle = "height: 40px; width: 40px; float: right; margin: -32px 5px 0px 0px;";       sendChat("", "/desc <div style='" + AlertOuterStyle + "'><div style='" + AlertInnerStyle + "'>" + AlertTokenName + " is Inspired! </div><img src='https://s3.amazonaws.com/files.d20.io/images/39783029/-w45_4ICV9QnFzijBimwKA/max.png' style='" + AlertImageStyle + "'></img></div>");                              //Set Status Icon on Token               getCharacterTokens(character.id).forEach((t)=>t.set('status_black-flag',true));              };              const dropInspiration = function (id) {       //Remove Status Icon from Token       if ( _.isEmpty(id)) return;       var character = getObj("character", id);              getCharacterTokens(character.id).forEach((t)=>t.set('status_black-flag',false));       };                     on("chat:message", function(msg) {           //Is script running?       if(msg.type == "api" && msg.content.indexOf("!inspiration") !== -1) {        sendChat("", "/desc <div style='max-height: 40px; width: 100%; margin: 10px 0px 5px -7px; line-height: 40px;'><div style='max-height: 20px; width: 100%; margin: 0px; padding: 0px 0px 2px 0px; clear: both; overflow: hidden; font-family: Candal; font-weight: lighter; font-size: 13px; line-height: 20px; color: fff; background-color: 20b2aa; background-image: linear-gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); border: 1px solid #000; border-radius: 4px; text-shadow: -1px -1px 0 000, 1px -1px 0 000, -1px 1px 0 000, 1px 1px 0 000'>Inspiration Tracking Online!</div></div>");       }       });                     // Has a character sheet been updated?       on("change:attribute", function(obj) {           var isupdated = obj.get("name");       //Was it the Inspiration attribute?       if (isupdated =="inspiration"){       var val = obj.get("current");       var id = obj.get("_characterid");         //Gained Inspiration?       if (isupdated =="inspiration" && val == "on"){           // Announce Character Inspired       announceInspiration(id);       }else if (isupdated =="inspiration" && val != "on"){       //Remove Inspiration Icon from Token       dropInspiration(id);       }              }              });                                   log('-=> HazInspiration <=- V0.2');                       sendChat("", "/desc <div style='max-height: 40px; width: 100%; margin: 10px 0px 5px -7px; line-height: 40px;'><div style='max-height: 20px; width: 100%; margin: 0px; padding: 0px 0px 2px 0px; clear: both; overflow: hidden; font-family: Candal; font-weight: lighter; font-size: 13px; line-height: 20px; color: fff; background-color: 20b2aa; background-image: linear-gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); border: 1px solid #000; border-radius: 4px; text-shadow: -1px -1px 0 000, 1px -1px 0 000, -1px 1px 0 000, 1px 1px 0 000'>Inspiration Tracking Online!</div></div>");                     });       
Thanks for this! I'll be giving it a shot in my game - I'm terrible at remembering to reward inspiration, so seeing a visual when players have it on their token might remind me to actually reward it!
Lady Victoria said: This isn't directly helpful to the API script in question, but on the subject of "how I handle inspiration in my game," I use this card I made in Photoshop as a 1-card, infinite-size deck in Roll20 and just drag cards onto players' portraits when they receive inspiration. That the card appears above their portrait is a nice visual cue to everyone. I do something similar.  Though my deck has two cards in it.  One for Inspiration, and one for Luck tokens.