This snippet manages some aspects of token HP Bars: Keep HP bar from going negative Keep HP bar from going over max Mark tokens that move to 0 HP with a statusmarker (default: dead , the red X) If a token represents a character and has the HP bar linked to that character: Mark tokens with a status marker (default: chained-heart ) if their character has a value for the "temp_hp" attribute greater than 0. If the value is between 1 and 9, the number of "temp_hp" will be on the marker. Automatically adjust reductions to HP so that they come from the "temp_hp" attribute first. The practical upshot of all this is it makes temporary HP easy to use and hard to forget about. It also observes TokenMod and ChatSetAttr so that if you make a change with them, it will do the right thing. This works out of the box with the Shaped 5e DnD sheet, using bar 3 as the health, but can be easily adjusted using the 4 variables at the top. Code: /* global TokenMod, ChatSetAttr */
on('ready', () => {
// Configuration parameters
const HPBarNum = 3;
const TempHPMarker = 'chained-heart';
const DeadMarker = 'dead';
const TempHPAttributeName = 'temp_hp';
/////////////////////////////////////////////
const bar = `bar${HPBarNum}_value`;
const lnk = `bar${HPBarNum}_link`;
const max = `bar${HPBarNum}_max`;
const mrk = `status_${TempHPMarker}`;
const ded = `status_${DeadMarker}`;
const checkTempHP = (obj) => {
let v = parseFloat(obj.get('current'));
findObjs({
type: 'graphic',
represents: obj.get('characterid')
})
.filter( (t) => t.get(lnk) !== '')
.forEach((g)=>{
g.set(mrk,(v>0 ? (v>9 ? true : v) : false) );
});
};
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;
}
}
if(hp > hpMax) {
hp = hpMax;
changes[bar] = hp;
changes[ded] = false;
} else if(hp <= 0) {
hp=0;
changes[bar] = hp;
changes[ded] = true;
} else {
changes[ded] = false;
}
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();
});
I combined this with my OnMyTurn snippet to implement my Order of the Immortal Mystic's temp HP every round. =D Enjoy! Support my work on If you use my scripts, want to contribute, and have the spare bucks to do so , go right ahead. However, please don't feel like you must contribute just to use them! I'd much rather have happy Roll20 users armed with my scripts than people not using them out of some sense of shame. Use them and be happy, completely guilt-free! Disclaimer: This Patreon campaign is not affiliated with Roll20; as such, contributions are voluntary and Roll20 cannot provide support or refunds for contributions.