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

Temporary Hit Point - Best API/Use of Tokenmod

I have been searching around for a script on how to best handle temporary hit points. Most of the post ive seen have been 4+ years old. Can someone point me in the right direction?
1586459235

Edited 1586469763
The Aaron
Roll20 Production Team
API Scripter
I have this little script I wrote for it... /* global TokenMod, ChatSetAttr */ on('ready', () => { // Configuration parameters const HPBarNum = 3; const TempHPMarker = 'chained-heart'; const DeadMarker = 'dead'; const TempHPAttributeName = 'temp_hp'; ///////////////////////////////////////////// const clearURL = /images\/4277467\/iQYjFOsYC5JsuOPUCI9RGA\/.*.png/; const bar = `bar${HPBarNum}_value`; const lnk = `bar${HPBarNum}_link`; const max = `bar${HPBarNum}_max`; 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 checkTempHP = (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[TempHPMarker]=v; } else { delete sm[TempHPMarker]; } g.set({ statusmarkers: packSM(sm) }); }); }; const assureTempHPMarkers = () => { let queue = findObjs({ type: 'attribute', name: TempHPAttributeName }); const burndownQueue = ()=>{ if(queue.length){ let attr = queue.shift(); checkTempHP(attr); setTimeout(burndownQueue,0); } }; burndownQueue(); }; const temporalTempHPCache = {}; const accountForHPBarChange = (obj,prev) => { // 1. did hp change and is it a scale const hpMax = parseInt(obj.get(max),10); let hp = parseInt(obj.get(bar),10); const diff = hp-parseFloat(prev[bar]); if( !isNaN(hpMax) && 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 temp hp const temp_hp = findObjs({ type: 'attribute', characterid: character.id, name: TempHPAttributeName })[0]; if( temp_hp ) { const now = Date.now(); // 5. have we accounted for it. if( !temporalTempHPCache.hasOwnProperty(character.id) || (now-temporalTempHPCache[character.id].when)>300 ) { // calculate necessary change const tempHP = parseFloat(temp_hp.get('current'))||0; const newTmpHP = Math.max((tempHP+diff),0); const toHeal = tempHP - newTmpHP; temporalTempHPCache[character.id]={ when: now, toHeal: toHeal }; temp_hp.set('current', newTmpHP); checkTempHP(temp_hp); } hp += temporalTempHPCache[character.id].toHeal; changes[bar] = hp; } } let sm = unpackSM(obj.get('statusmarkers')); if(hp > hpMax) { hp = hpMax; changes[bar] = hp; delete sm[DeadMarker]; } else if(hp <= 0) { hp=0; changes[bar] = hp; sm[DeadMarker] = true; } else { delete sm[DeadMarker]; } changes.statusmarkers = packSM(sm); obj.set(changes); } }; const onAttributeChange = (obj) => { if(obj.get('name') === TempHPAttributeName){ checkTempHP(obj); } }; on("change:attribute", onAttributeChange); on("change:token", accountForHPBarChange); if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(accountForHPBarChange); } if('undefined' !== typeof ChatSetAttr && ChatSetAttr.registerObserver){ ChatSetAttr.registerObserver('change',onAttributeChange); } assureTempHPMarkers(); });
Once again The Aaron saves the day! 
Ok installed..... im very uuhhh call me a simpleton when it comes to this stuff..... How do I use it? :D <3
1586462371
The Aaron
Roll20 Production Team
API Scripter
Edit these: // Configuration parameters const HPBarNum = 3; const TempHPMarker = 'chained-heart'; const DeadMarker = 'dead'; const TempHPAttributeName = 'temp_hp'; In order: Which bar you want to use for hit points What status marker to apply to tokens with temporary hit points What status marker to apply to tokens with 0 hit points What Attribute your character sheet uses for temporary hit points. Then you just put temporary hit points in your character sheet, and the script will mark the token with the status marker so you know they're there.  If the number is between 1-9, it will show the number on the status marker.  If it's greater, it won't show a number.  When you take away hit points from the bar for hit points, the script will rebalance the difference against temp hit points.  It will also prevent the HP from going above max or below 0, and mark tokens that are dead at 0 hit points.
So basically for OGL just change the 'temp_hp' to 'hp_temp' (lol) and then if say warlock uses armor of agathys they would put +5 into bar3 and the script will do the rest? Just want to make sure Im following correctly :D once again you are the mannnnn. I hope one day to have JS knowledge like you. 
1586462937

Edited 1586462964
I didn't ask for this, but I'm sure taking it! Thank you, API God ...I mean The Aaron. I've flirted with learning to code javascript in the past but the idea of being able to work up my own custom scripts or sheets for Roll20 might just be enough to push me to actually do it.
And change that attribute throughout the entire code im assuming. 
1586463063
The Aaron
Roll20 Production Team
API Scripter
If you tie bar 3 to hp_temp, that will definitely work.  (Assuming bar 1 is your HP?) Basically, you can't add Temp HP via the HP bar, because I can't tell the difference between adding temporary hit points and healing, so I couldn't prevent healing from pushing you over the max.  If you link bar 3 to the hp_temp attribute, you can individually adjust the temp hit points.  Otherwise, you have to do it on the character sheet or with a script.  I use ChatSetAttr to do the temp hp, or just put it in the character sheet. The script respects changes that happen from ChatSetAttr and from TokenMod, so you can keep using those to adjust stuff with impunity. =D
Currently I have the icons activating however it is not balancing the HP. 17/17hp +5/5temphp -7 gets 10/17 5/5temphp. It has also permanently marked the character as dead? lol 
1586463769
The Aaron
Roll20 Production Team
API Scripter
hmm.  if you wanna PM me an invite and GM me, I'll come sort it out. 
Sounds good! 
Okay, I just tested on a game with no api... if you have hp bar showing and add HP above standard for token, roll20 adds a + to the right side of your HP bar.
1586469799
The Aaron
Roll20 Production Team
API Scripter
Ok, fixed it.  I updated the code above.  I was using the old way for status markers for some reason... =D
1586505650
Dumbhuman
Pro
Marketplace Creator
The old Flight script allows for easily adding additional commands to its code, so I also use it for adding temporary hit points as well as for storing the result of Stealth rolls when a character hides.&nbsp; I don't typically automate hit point changes on tokens, so this method works well enough for me.&nbsp; If a character has 15 temporary hit points, I just select the token and type: !temp 15 If they roll a 23 on their Stealth check to hide, I select the token and type: !hide 23 And of course if they're flying 70 feet in the air, I select the token and type: !fly 70 Funny enough though, I use the Flight script for temporary hit points and hiding a lot more than I use it for flying. Here's the version I use: var bshields = bshields || {}; bshields.flight = (function() { 'use strict'; var version = 3.5, commands = { fly: function(args, msg) { var height = parseInt(args[0]) || 0; markStatus('fluffy-wing', height, msg.selected); }, hide: function(args, msg) { var stealth = parseInt(args[0]) || 0; markStatus('ninja-mask', stealth, msg.selected); }, temp: function(args, msg) { var tempHP = parseInt(args[0]) || 0; markStatus('red', tempHP, msg.selected); }, /** * To add new command, use this template: commandname: function(args, msg) { var num = parseInt(args[0]) || 0; markStatus('statusmarker-name', num, msg.selected); }, * Statusmarker names are listed at <a href="https://wiki.roll20.net/API:Objects#Graphic_.28Token.2FMap.2FCard.2FEtc..29" rel="nofollow">https://wiki.roll20.net/API:Objects#Graphic_.28Token.2FMap.2FCard.2FEtc..29</a> * commandname should be ALL LOWER-CASE and CANNOT contain spaces. If commandname includes anything other than a-z0-9_ * or if it begins with a number, it must be enclosed in quotes, eg: 'command-name': function... */ help: function(command, args, msg) { if (_.isFunction(commands[`help_${command}`])) { commands[`help_${command}`](args, msg); } }, help_fly: function(args, msg) { sendChat(`Flight v${version}`, 'Specify !fly &amp;'+'lt;number&amp;'+'gt; to add that number as wings on the selected token.'); } }; function markStatus(marker, num, selected) { var markerStr = '', token, markers; if (!selected) return; selected = _.reject(selected, (o) =&gt; o._type !== 'graphic'); if (!selected.length) return; if(num) { markerStr = _.chain(num.toString().split('')) .map((d) =&gt; `${marker}@${d}`) .value() .join(','); } _.each(selected, (obj) =&gt; { token = getObj('graphic', obj._id); if (token &amp;&amp; token.get('subtype') === 'token') { token.set(`status_${marker}`, false); markers = token.get('statusmarkers'); markers = markers ? markers.trim() : ''; markers += (markers.length ? ',' : '') + markerStr; token.set('statusmarkers', markers); } }); } function handleInput(msg) { var isApi = msg.type === 'api', args = msg.content.trim().splitArgs(), command, arg0, isHelp; if (isApi) { command = args.shift().substring(1).toLowerCase(); arg0 = args.shift() || ''; isHelp = arg0.toLowerCase() === 'help' || arg0.toLowerCase() === 'h' || command === 'help'; if (!isHelp) { if (arg0 &amp;&amp; arg0.length &gt; 0) { args.unshift(arg0); } if (_.isFunction(commands[command])) { commands[command](args, msg); } } else if (_.isFunction(commands.help)) { commands.help(command === 'help' ? arg0 : command, args, msg); } } else if (_.isFunction(commands['msg_' + msg.type])) { commands['msg_' + msg.type](args, msg); } } function registerEventHandlers() { on('chat:message', handleInput); } return { registerEventHandlers: registerEventHandlers }; }()); on('ready', function() { 'use strict'; bshields.flight.registerEventHandlers(); });
1586525534
The Aaron
Roll20 Production Team
API Scripter
That's some clever repurposing, KC!