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?
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?
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(); });
Ok installed..... im very uuhhh call me a simpleton when it comes to this stuff..... How do I use it? :D <3
Edit these:
// Configuration parameters const HPBarNum = 3; const TempHPMarker = 'chained-heart'; const DeadMarker = 'dead'; const TempHPAttributeName = 'temp_hp';
In order:
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.
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.
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
Ok, fixed it. I updated the code above. I was using the old way for status markers for some reason... =D
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. I don't typically automate hit point changes on tokens, so this method works well enough for me. 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 https://wiki.roll20.net/API:Objects#Graphic_.28Token.2FMap.2FCard.2FEtc..29
* 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 &'+'lt;number&'+'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) => o._type !== 'graphic');
if (!selected.length) return;
if(num) {
markerStr = _.chain(num.toString().split(''))
.map((d) => `${marker}@${d}`)
.value()
.join(',');
}
_.each(selected, (obj) => {
token = getObj('graphic', obj._id);
if (token && 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 && arg0.length > 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();
});