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

resource tracking (character sheet or playing cards via API)

Hey everyone, We're using a google document for general inventory tracking, but for spells, etc. we want a roll20 internal solution. Opening the PF character sheet manually to change the number of charges, available spells or quantity is too inconvenient, as even with the improved speed with our sometimes slow internet connection the sheet can take too long to navigate. Our group has moved to a card system, but you still have to remember to play the card. I'm wondering if it's possible to place a card from the hand to the tabletop (perhaps on selected token while calling the script) via API command (that could be included in macros such that when a spell is cast a card is played automatically).  Also interesting to me is the opposite: Can playing the card trigger a macro? I imagine playing a card and having the macro trigger from that would be fun, and this would leave the choice of where to place the card in human hands. Can API easily change things within the character sheet directly? If that was the case it might be worth to skip the cards and just have the macros change the values themselves. Fire a bow and lose an arrow, cast a spell... you get the point. What do you use for resource tracking? Thank you, Julian
1454334445
The Aaron
Pro
API Scripter
I'll try to answer some of your questions: 1) Cards running macros -- Can be done, but it's not very easy.  Macro expansion bye the API is not for the faint of heart.  Let me break down the complications: In general, @{selected} and @{target} have no way to get data. With @{selected} and an ability, we can assume it's the token in question, but @{target} we don't have anything. We'd either need to drop it or prompt the user with an API Button (more on that later). Roll queries like ?{What do you want to do with this?|x|y|z} can't be done in the API, no access to the players. We could choose the default value (if one is given), or prompt with an API Button (more on that later). API Commands could be challenging, particularly because we don't really know who to run them as. @{attribute} references would all need to be rewritten as @{character|attribute} so they resolve correctly. (I actually already have code that does this in GroupInitiative and PCPP) Various chat commands could be problematic For the API Buttons mentioned above, you'd walk the whole ability/macro to start with, then either extract all the target and query bits and do them as a single command, roughly: [More Details](!moredetails 123412 ?{What do you want to do with this?|x|y|z} @{target|Shoot Who?|token_id}) or you'd pass them as individual fill ins: <b>What do you want to do with this?</b>[x](!moredetails 123412 q1 x)[y](!moredetails 123412 q1 y)[z](!moredetails 123412 q1 z) <b>Which target?</b>[Choose](!moredetails 123412 t1 @{target|Shoot Who?|token_id}) 2) Macros create cards -- This is easier and also partially impossible.  The API cannot create images that are not in a user's library (i.e.: no market place images).  If all your card images are in your own personal library, this could be done.  One other complication is that the Player's Hand is read only, so while a card could be played, it couldn't be removed from the player's hand. 3) API changes the sheet directly -- This is the easiest of the 3.  I've got the  Ammo script and there is one named attrib floating about.
I'm currently using several attributes on the Shaped 5e Character Sheet to track hit dice expended and then renew them at long rests or via DM fiat.  I didn't have any luck with the hit die attributes, so I've co-opted the last 4 inventory slots, but it's persistent and easily accessible.
David M. said: I'm currently using several attributes on the Shaped 5e Character Sheet to track hit dice expended and then renew them at long rests or via DM fiat.  I didn't have any luck with the hit die attributes, so I've co-opted the last 4 inventory slots, but it's persistent and easily accessible. Wait, so the sheet itself can do that? Or do you have a script? - this sounds like what I'm looking for. Hit one botten and everyone's daily uses go back up... - hp healing is weird anyway in Pathfinder, so having to do that manually wouldn't be too bad...
1454377028

Edited 1454396374
The Aaron said: [...] This does sound like a whole bunch more work than I'm willing to do... but the ammo script looks awesome. I'll take a look at it when I'm less tired.  Edit: Did briefly. Also  this guy mentioned using the script to apply to a bunch of seemingly pathfinder related things, but didn't post it. A bit later someone shared a "more generic" version but it only changed some of the labels apparently... - and then there's one poster who asked a lot of great questions (and great fixes by you) leading to whispers that would have to wait until after the ammo over-haul... this being a year ago, did that ever get added? I too would like to have the thing tell me that I spent an arrow (or spell), but not bug other players with it, and if it's 100 % or 0% recoverable not even the GM. He'd just trust that the macro is set up right to substract use where needed. He really just needs to know whether they rolled to be recoverable, so that if the character is willing to go through the hasstle of recovering, they can...  But as the other commenter had said, that's a long-shot icing on the cake! - the script is awesome and I'll try and give it some of my time soon. If anyone else has worked on any improvements since, I'd be happy to see them, as surely would others. Thanks again, Julian
1454415415
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
Julix said: The Aaron said: [...] This does sound like a whole bunch more work than I'm willing to do...  That pretty much sums up every conversation I have ever had with Aaron.
1454437624

Edited 1454439476
Stephen S. said: Julix said: The Aaron said: [...] This does sound like a whole bunch more work than I'm willing to do...  That pretty much sums up every conversation I have ever had with Aaron. Almost, what's missing is a recommendation to read  this. From the book: "This is not a book for beginners. Someday I hope to write a JavaScript: The First Parts book, but this is not that book. This is not a book about Ajax or web programming. The focus is exclusively on JavaScript, which is just one of the languages the web developer must master. This is not a book for dummies. This book is small, but it is dense. There is a lot of material packed into it. Don’t be discouraged if it takes multiple readings to get it. Your efforts will be rewarded." I still consider myself a beginner, and don't want to disprove that I'm smart by trying but failing to grasp the concepts in the book... if I remember it exists on a "high self confidence day" I'll give it a try :-P  Edit: Bahahaha "The treatment here is not exhaustive. It avoids the edge cases. You should, too. There is danger and misery at the edges." - ups I'm reading it, and I should be rushing to class... And again, thanks Aaron for everything you keep doing for this community!
1454437800
The Aaron
Pro
API Scripter
HA!  Yeah, true enough. =D
Julix said: David M. said: I'm currently using several attributes on the Shaped 5e Character Sheet to track hit dice expended and then renew them at long rests or via DM fiat.  I didn't have any luck with the hit die attributes, so I've co-opted the last 4 inventory slots, but it's persistent and easily accessible. Wait, so the sheet itself can do that? Or do you have a script? - this sounds like what I'm looking for. Hit one botten and everyone's daily uses go back up... - hp healing is weird anyway in Pathfinder, so having to do that manually wouldn't be too bad... I have a script...well some scripts.  PLEASE excuse my terrible scripts.  I don't really javascript well (I just started learning so I could use the API), so most of this is an amalgamation of existing scripts, google and stackexchange.  >.>   (I'm so embarrassed that The Aaron will see these.) //Hit Die Script on ("chat:message", function (msg) { if (msg.type == "api" && msg.content === "!hitdie") {                     var tselection = msg.selected;                     _.each(tselection, function(obj) {                         var objsource = getObj("graphic", obj["_id"]);                         var characterId = objsource.get("represents");                         var character = getObj("character", characterId);                         var chardie = getAttrByName(character.id, "inventorydescription59");                         var conmod = getAttrByName(character.id, "inventorydescription58");                         var currhitdie = getAttrByName(character.id, "inventorydescription60");                         var maxhitdie = getAttrByName(character.id, "inventorydescription57");                         var spenttoday = getAttrByName(character.id, "inventorydescription56");                         var currhealth = objsource.get("bar3_value");                         var maxhealth = objsource.get("bar3_max");                         var hurt = ((1*maxhealth)-(1*currhealth));                         var tokenname = objsource.get("name");                         //insert new blocks here                         if(1* currhitdie > 0){                                                      if (tokenname.match("Rak|Lenora|Mildred|Noz|Coll")) {  //deals with non-ceridwen player rolls                                     sendChat('', '/roll [[1'+chardie+']]', function (ops) {                                     rollresult = JSON.parse(ops[0].content)                                      var total = rollresult.total                                         if (1*total  + 1*conmod < 1*hurt){                                             objsource.set({bar3_value: (1*(1*currhealth + 1*total+1*conmod))});                                         }                                         else if(total  + conmod >hurt){                                             objsource.set({bar3_value: (1*maxhealth)});                                         }                                         if (1*currhitdie >1){                                             newcurrhitdie = ((1* currhitdie) - 1);                                             //sendChat ('', "test value = " + newcurrhitdie);                                             setAttr2 ('inventorydescription60', newcurrhitdie, characterId)                                         }                                                 else if(1*currhitdie == 1){                                             newcurrhitdie = 0                                             setAttr2 ('inventorydescription60', '0' ,characterId)                                         }                                 sendChat('', '@{' + tokenname +'|output_option} &{template:5eDefault} {{character_name=@{' + tokenname +'|character_name}}} @{' + tokenname +'|show_character_name} {{title=Spending Hit Dice}} {{subheader='+chardie+'}} {{rollname=HP regained}} {{roll=[[' + total + ' + @{' + tokenname +'|constitution_mod}]]}} {{effect=You have '+newcurrhitdie+' hit dice remaining}} @{' + tokenname +'|classactionhitdice}')                                 setAttr2 ('inventorydescription56', (1*spenttoday + 1) ,characterId)                                                                      })                             }                                                      else if (tokenname == "Ceridwen") {  //deals with ceridwen non-beast shape player rolls                                     sendChat('', '/roll [[1'+chardie+']]', function (ops) {                                     rollresult = JSON.parse(ops[0].content)                                     var total = rollresult.total                                         if (1*total  + 1*conmod < 1*hurt){                                             objsource.set({bar3_value: (1*(1*currhealth + 1*total+1*conmod))});                                         }                                         else if(total  + conmod >hurt){                                             objsource.set({bar3_value: (1*maxhealth)});                                         }                                         if (1*currhitdie >1){                                         newcurrhitdie = ((1* currhitdie) - 1);                                         setAttr2 ('inventorydescription60', newcurrhitdie,characterId)}                                                 else if(1*currhitdie == 1){                                         newcurrhitdie = 0                                         setAttr2 ('inventorydescription60', '0', characterId)}                                                         sendChat('', '@{' + tokenname +'|output_option} &{template:5eDefault} {{character_name=@{' + tokenname +'|character_name}}} @{' + tokenname +'|show_character_name} {{title=Spending Hit Dice}} {{subheader='+chardie+'}} {{rollname=HP regained}} {{roll=[[' +total+ ' + @{' + tokenname +'|constitution_mod}]]}} {{effect=You have '+newcurrhitdie+' hit dice remaining}} @{' + tokenname +'|classactionhitdice}')                                 setAttr2 ('inventorydescription56', (1*spenttoday + 1) ,characterId)                             })                             }                                                          else if ( tokenname.indexOf( 'Ceridwen-' ) > -1 ) { //deals with  Ceridwen beast shape                                     sendChat ('', "Trying the beastshape version");                                     sendChat('', '/roll [[1'+chardie+']]', function (ops) {                                     rollresult = JSON.parse(ops[0].content)                                     var total = rollresult.total                                         if (1*total  + 1*conmod < 1*hurt){                                             objsource.set({bar3_value: (1*(1*currhealth + 1*total+1*conmod))});                                         }                                         else if(1*total  + 1*conmod >hurt){                                             objsource.set({bar3_value: (1*maxhealth)});                                         }                                         if (1*currhitdie >1){                                         newcurrhitdie = ((1* currhitdie) - 1);                                         setAttr2 ('inventorydescription60', newcurrhitdie, characterId)}                                                 else if(1*currhitdie == 1){                                         newcurrhitdie = 0                                         setAttr2 ('inventorydescription60', '0' , characterId)}                                             sendChat  ('',  '@{Ceridwen|output_option} &{template:5eDefault} {{character_name=@{Ceridwen|character_name}}} @{Ceridwen|show_character_name} {{title=Spending Hit Dice}} {{subheader='+chardie+'}} {{rollname=HP regained}} {{roll=[[' +total+ ' + @{Ceridwen|constitution_mod}]]}} {{effect=You have '+newcurrhitdie+' hit dice remaining}}@{Ceridwen|classactionhitdice}')                                         setAttr2 ('inventorydescription56', (1*spenttoday + 1) ,characterId)                                                              })                         }                         else { sendChat ('', 'This is not a Player Character.');}                                                  }                                                  else if (1* currhitdie < 1){sendChat ('', "Sorry.  You have no hit die remaining. Please don't die.")}                                                  //end insert new blocks                         })                        // setAttribute ('inventorydescription40', 'd8')                         //hitdietype = ;                         // objsource.set({bar3_value: newcurrhealth});                         // objsource.set({bar3_max: 27});                          //objsource.set({name: 'Boopsnout'}); //begins setAttr2 function  function setAttr2(name, currentVal, characterId) {         if (!name) {             throw('Name required to set attribute');         }         max = '';         if (!currentVal) {             sendChat('','Error setting empty value: ' + name);             return;         }         var attr = findObjs({             _type: 'attribute',             _characterid: characterId,             name: name         })[0];         if (!attr) {             //log('Creating attribute ' + name);             createObj('attribute', {                 name: name,                 current: currentVal,                 max: max,                 characterid: characterId             });         } else if (!attr.get('current') || attr.get('current').toString() !== currentVal) {             //log('Updating attribute ' + name);             attr.set({                 current: currentVal,                 max: max             });         }     };  //ends setAttribute function                                                                                                                                                                          }}); //Hit Die Reset Script on ("chat:message", function (msg) { if (msg.type == "api" && msg.content === "!hdreset") {                     var tselection = msg.selected;                     i = 0                     _.each(tselection, function(obj) {                          objsource = getObj("graphic", obj["_id"]);                          characterId = objsource.get("represents");                          character = getObj("character", characterId);                          maxhitdie = getAttrByName(character.id, "inventorydescription57");                          maxhealth = objsource.get("bar3_max");                         //insert new blocks here                         setAttr2('inventorydescription56', '0', characterId)                         setAttr2('inventorydescription60', maxhitdie, characterId)                         objsource.set("bar3_value", maxhealth)                     i++                           //end insert new blocks                     })                         sendChat(msg.who, "All Better.") //begins setAttr2 function  function setAttr2(name, currentVal, characterId) {         if (!name) {             throw('Name required to set attribute');         }         max = '';         if (!currentVal) {             sendChat('','Error setting empty value: ' + name);             return;         }         var attr = findObjs({             _type: 'attribute',             _characterid: characterId,             name: name         })[0];         if (!attr) {             //log('Creating attribute ' + name);             createObj('attribute', {                 name: name,                 current: currentVal,                 max: max,                 characterid: characterId             });         } else if (!attr.get('current') || attr.get('current').toString() !== currentVal) {             //log('Updating attribute ' + name);             attr.set({                 current: currentVal,                 max: max             });         }     };  //ends setAttribute function                                                                                                                                                                          }}); //Long Rest Script   on ("chat:message", function (msg) { if (msg.type == "api" && msg.content === "!longrest") {                     var tselection = msg.selected;                     _.each(tselection, function(obj) {                          objsource = getObj("graphic", obj["_id"]);                          characterId = objsource.get("represents");                          character = getObj("character", characterId);                          maxhitdie = getAttrByName(character.id, "inventorydescription57");                          spenttoday = getAttrByName(character.id, "inventorydescription56");                          currhitdie = getAttrByName(character.id, "inventorydescription60");                          maxhealth = objsource.get("bar3_max");                          recoverhd = Math.floor(spenttoday/2);                          mystatuses = objsource.get("statusmarkers");                                                  //insert new blocks here                         if ((1* currhitdie + 1 * recoverhd) > maxhitdie){                             setAttr2('inventorydescription60', maxhitdie, characterId);                         }                         else {                             setAttr2('inventorydescription60', (1*currhitdie + 1*recoverhd), characterId);                         }                         if (getAttrByName(character.id, "inventorydescription60") == maxhitdie) {                             setAttr2('inventorydescription56', '0', characterId);                         }                         objsource.set({bar3_value: (1*maxhealth)});                         newhitdice = getAttrByName(character.id, "inventorydescription60");                         sendChat(msg.who, "That was a nice nap. You now have " + maxhealth + " hit points and " + newhitdice + " hit dice.");                         if(mystatuses.match(/half/gi)) {                             //sendChat(msg.who, "!token-mod --set statusmarkers|!half-heart")                            sendChat(msg.who, '[You are less exhausted. Click Here.](!token-mod --set statusmarkers|?half-heart:-1)');                         }                         //end insert new blocks                     })                          //begins setAttr2 function  function setAttr2(name, currentVal, characterId) {         if (!name) {             throw('Name required to set attribute');         }         max = '';         if (!currentVal) {             sendChat('','Error setting empty value: ' + name);             return;         }         var attr = findObjs({             _type: 'attribute',             _characterid: characterId,             name: name         })[0];         if (!attr) {             //log('Creating attribute ' + name);             createObj('attribute', {                 name: name,                 current: currentVal,                 max: max,                 characterid: characterId             });         } else if (!attr.get('current') || attr.get('current').toString() !== currentVal) {             //log('Updating attribute ' + name);             attr.set({                 current: currentVal,                 max: max             });         }     };  //ends setAttribute function                                                                                                                                                                          }});    
1454525428
The Aaron
Pro
API Scripter
HA!  Not only have I SEEN worse, I've WRITTEN worse.  Not shame required!  There are many things you're doing that even seasoned programmers haven't adopted yet (using _.each(), matching with regular expressions, etc.). A great way to move to the next level is to read Javascript: The Good Parts by Douglas Crockford . (There you go, Julix!).  Also, if you post and ask for suggestions, we'll all be happy to give you constructive feedback. =D
The Aaron said: HA!  Not only have I SEEN worse, I've WRITTEN worse.  Not shame required!  There are many things you're doing that even seasoned programmers haven't adopted yet (using _.each(), matching with regular expressions, etc.). A great way to move to the next level is to read Javascript: The Good Parts by Douglas Crockford . (There you go, Julix!).  Also, if you post and ask for suggestions, we'll all be happy to give you constructive feedback. =D Thank you!
I forgot to mention:  In the HitDie script, there are if statements in the middle for different token names because we have a Druid with wildshape, and her token-name changes.  Unless you have the same scenario, you could just replace my character names with yours in the first if (tokenname.match statement. I have a whole different set of scripts for managing wildshapes...
1454583725

Edited 1454597285
David M. said: I forgot to mention:  In the HitDie script, there are if statements in the middle for different token names because we have a Druid with wildshape, and her token-name changes.  Unless you have the same scenario, you could just replace my character names with yours in the first if (tokenname.match statement. I have a whole different set of scripts for managing wildshapes... But character names do have to be set for this to work, right? Or could it be done with "@{selected|...}"? Nevermind, hadn't tried it yet. - !hdreset works :)  However I don't really understand what all the references to inventory are about...   "Error: No attribute or sheet field found for character_id -K9OsHO8WXijgn10XKf- named inventorydescription59" "Error: No attribute or sheet field found for character_id -K9OsHO8WXijgn10XKf- named inventorydescription58" "Error: No attribute or sheet field found for character_id -K9OsHO8WXijgn10XKf- named inventorydescription60" "Error: No attribute or sheet field found for character_id -K9OsHO8WXijgn10XKf- named inventorydescription57" "Error: No attribute or sheet field found for character_id -K9OsHO8WXijgn10XKf- named inventorydescription56" Edit: Just saw the Token-Mod tie-in (fatigue symbol) - so I went and added that (and it needed isGM, so that too) - suddenly I got a bunch of scripts :D Never knew you could add a number under the symbols, that is awesome!
1454594934

Edited 1454595118
Julix said: David M. said: I forgot to mention:  In the HitDie script, there are if statements in the middle for different token names because we have a Druid with wildshape, and her token-name changes.  Unless you have the same scenario, you could just replace my character names with yours in the first if (tokenname.match statement. I have a whole different set of scripts for managing wildshapes... But character names do have to be set for this to work, right? Or could it be done with "@{selected|...}"? Nevermind, hadn't tried it yet. - !hdreset works :)  However I don't really understand what all the references to inventory are about...   "Error: No attribute or sheet field found for character_id -K9OsHO8WXijgn10XKf- named inventorydescription59" "Error: No attribute or sheet field found for character_id -K9OsHO8WXijgn10XKf- named inventorydescription58" "Error: No attribute or sheet field found for character_id -K9OsHO8WXijgn10XKf- named inventorydescription60" "Error: No attribute or sheet field found for character_id -K9OsHO8WXijgn10XKf- named inventorydescription57" "Error: No attribute or sheet field found for character_id -K9OsHO8WXijgn10XKf- named inventorydescription56" It uses the selected token to obtain the token name to know which if statement to use. I should probably also have mentioned that I use Token Bar3 for HP; because I've got that linked to the character sheet, it interacts with the character sheet and changes that attribute for me. The inventory attributes are mostly for maintaining stats between sessions. I'm using the inventory slots because I found that using the HD information on the shaped character sheet did not consistently return good data.  I've allotted them as follows:  The Hit die attribute will actually become a problem once one of my party multi classes to something with a different hit die.  At that point, I'm probably going to have to re-write that section, but this is a starter party with all single-classes, so it's working for now. Hope that helps! -David
I don't see any inventory slots like that, where are those to be found? Oh right: 5e :D I had forgotten... Alright, I'll see what I can do to avoid getting "hd undefined". In the pathfinder sheet there's not currently a spot for HD as far as I know, because most races just use class levels instead of racial hd. Advantage is no new slot has to be allotted, it would just sum the various levels in various things that are present. - but I guess that means the rules will probably be different... HD Spent today? I'm not even sure what that means. 7 am. Time for sleep, but I'll look some more at it later. For now I'm happy that I can reset hp at command and in the future will be able to do rest thingies... Probably the spell restoring stuff can be built into part of the rest script, if so that will be awesome. Someday when I'll have time... While the pf version would still need some changes, looks like your 5e version is ready to be shared more broadly (i.e. in it's own thread with a link to github in the initial post), no?  The script is so good, it leaves me curious what other scripts you have... And more generally I now wonder how many more cool scripts are hidden away because not all people were courageous enough to show their codes, like you did :) (by the way not implying that lack of courage is the only reason not to share code, maybe you don't want to be bugged by people needing help to set it up or for elaborate things having copyright concerns, or what not, of course no one has to share any code. just mean if fear of judgement is the only thing keeping you from sharing, then courageously sharing your code anyway is great, as it has potential to make lives of other members of the community better. Thanks for doing just that, David!)