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

[Script] FF-style damage numbers

April 03 (6 years ago)
Quinn
Pro
Sheet Author

Do you like JRPGs? Do you miss the satisfying pop of those little numbers that fly up when someone takes damage? Now you can have them on Roll20!


This is a simple little script, but I was running a Final Fantasy based campaign, and I felt like it needed a little something extra. Now that it works, I may as well throw it up for others to use.

Features

  • Automatically works on any token with a health value
  • Customizable font
  • Customizable colors - separately for damage and healing values
  • Can choose any bar as the health bar
  • Can choose whether or not to display numbers for healing
  • Can choose whether or not to display numbers for tokens with no maximum health
  • Can choose whether or not to display numbers for tokens that aren't on the player page

Installation

Source code: Here

Github repo: Here

April 03 (6 years ago)
Spren
Sheet Author

I love it. Adding it to my game. 

April 03 (6 years ago)

Edited April 03 (6 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

That's neat! Cool idea.

Very Cool! Thanks for sharing!

April 04 (6 years ago)
Ziechael
Forum Champion
Sheet Author
API Scripter

Nice... visual aids are always great!

April 04 (6 years ago)
The Aaron
Roll20 Production Team
API Scripter

Spiffy!  I’ve got a few helper functions you might like, I’ll try to share them when I’m on my computer next. 

Are you interested in some feedback on things you could enhance?

April 04 (6 years ago)
Quinn
Pro
Sheet Author

Sure! I'm always happy to learn how to refine my craft.

April 04 (6 years ago)
The Aaron
Roll20 Production Team
API Scripter
Cool. I’ll get you some feedback with those functions. =D
April 04 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

One change I suggest, is changing this bit:

on('change:graphic', function(obj, prev) {
    if(state.damageNumbers.enabled) {
        let oldHp = 0;
        let newHp = 0;
        let oldHpMax = 0;
        let newHpMax = 0;
        
        switch(state.damageNumbers.healthBar) {
            case 1:
                oldHp = parseInt(prev.bar1_value);
                oldHpMax = parseInt(prev.bar1_max);
                newHp = parseInt(obj.get('bar1_value'));
                newHpMax = parseInt(obj.get('bar1_max'));
                break;
            case 2:
                oldHp = parseInt(prev.bar2_value);
                oldHpMax = parseInt(prev.bar2_max);
                newHp = parseInt(obj.get('bar2_value'));
                newHpMax = parseInt(obj.get('bar2_max'));
                break;
            case 3:
                oldHp = parseInt(prev.bar3_value);
                oldHpMax = parseInt(prev.bar3_max);
                newHp = parseInt(obj.get('bar3_value'));
                newHpMax = parseInt(obj.get('bar3_max'));
                break;
        }

to

on('change:graphic', function(obj, prev) {
if(state.damageNumbers.enabled) {
const bar = `bar${state.damageNumbers.healthBar}_`,
oldHp = parseInt(prev[bar + 'value']),
oldHpMax = parseInt(prev[bar + 'max']),
newHp = parseInt(obj.get(bar + 'value')),
newHpMax = parseInt(obj.get(bar + 'max'));

or

on('change:graphic', function(obj, prev) {
if(state.damageNumbers.enabled) {
const bar = which => `bar${state.damageNumbers.healthBar}_${which}`,
oldHp = parseInt(prev[bar('value')]),
oldHpMax = parseInt(prev[bar('max')]),
newHp = parseInt(obj.get(bar('value'))),
newHpMax = parseInt(obj.get(bar('max')));

I'm keen to see Aaron's feedback too (and whether he'd suggest something better here).
April 05 (6 years ago)

Edited April 05 (6 years ago)
The Aaron
Roll20 Production Team
API Scripter

Yup, that was one of the things I was going to mention.  =D

Like GiGs pointed out, you can reduce code duplication by building the property names based on the healthBar configuration.  It's a minor thing, but I'd actually build them once outside the function and and save them:

    const barValue = `bar${config.healthBar}_value`;
    const barMax = `bar${config.healthBar}_max`;
then use it in the function:
            const oldHp = parseInt(prev[barValue]);
            const oldHpMax = parseInt(prev[barMax]);
            const newHp = parseInt(obj.get(barValue));
            const newHpMax = parseInt(obj.get(barMax));
(I've gotten to where I prefer individually declaring the type of variables, hence the const on each line.  That's just personal preference, but it makes reordering and moving things simple.)

Another thing to be aware of is the 'ready' event.  When the API starts up, it sends an add event for every object in the entire game.  That's not a big deal for this script, but if you were writing one that took some action on the creation of something, you'd not want to have it activate on all the loading create events.  To get around that, you wrap your script in:
on('ready',()=>{

   /* your script */

});

The 'ready' event happens only once, right after all objects have been loaded in the game.

Wrapping in a function scope has an added benefit:  All scripts in the API are concatenated into a single file, which means there is a single global namespace.  That means any functions you put in the global namespace could collide with other functions.  Wrapping in a 'ready' event puts those in a function namespace so you don't have to worry about polluting the global namespace. =D

So, I mentioned sharing some functions.  I notice you're checking the player page if requirePlayerPage is set.  There are technically 3 possible settings for pages.  There's the playerpageid, the playerspecificpages, and the lastpage on a player.  I wrote a function that builds the right list:

    const getActivePages = () => [
        ...new Set([
            Campaign().get('playerpageid'),
            ...Object.values(Campaign().get('playerspecificpages')),
            ...findObjs({
                type: 'player',
                online: true
            })
            .filter((p)=>playerIsGM(p.id))
            .map((p)=>p.get('lastpage'))
        ])
    ];
This returns a list of pages that have players on them, so you can check if the object is on any of those:
                let activePages = getActivePages();

                if(!activePages.includes(obj.get('_pageid'))) {
                    return;
                }

So, now about the state object.  The state object is intended to store values that are persisted across loads of the api.  It's ideal for settings, but ONLY if there is a way to set them at some point.  To paraphrase The King of Attolia by Megan Whalen Turner (which is a great series) when you constantly overwrite it on load "you thwart its purpose."  In your case, you are better off just having a regular old object for configuration:
    const config = {
        enabled: true,              // Set to false to disable script
        font: 'Contrail One',       // Choices: 'Arial', 'Patrick Hand', 'Contrail One', 'Shadows Into Light', 'Candal'
        damageColor: 'black',       // Color of damage numbers
        healingColor: 'green',      // Color of healing numbers
        healthBar: 1,               // Which bar is the health bar?
        displayHealing: true,       // Show green numbers when health increases
        requireMaxHealth: false,    // Only show damage numbers if the token has a maximum health
        requirePlayerPage: true     // Only show damage numbers on the active player page
    };
To truly make use of the state object, you'll want to only selectively initialize it, and provide ways to set it.  I wrote a whole section on this in the wiki: https://wiki.roll20.net/API:Objects#state

The only other comment I have, which is really a minor thing in this case, is that you often want to do your short-circuit tests as early as possible.  There's no point in calculating the various hit points if you are not going to be active on that page, so moving the page check earlier than the hp calculations is a minor efficiency gain.

Here's the script, reformatted with the above, though I'm gaming right now, so I haven't tested it. =D
/* Damage Numbers script
 * Generates Final Fantasy-style damage numbers whenever a token's health increases or decreases.
 * Works for any system.
 * 
 * Created by Quinn Gordon
 * Roll20: https://app.roll20.net/users/42042/prof
 * Github: https://github.com/ProfessorProf
 */

on('ready', ()=>{
    // Config:
    const config = {
        enabled: true,              // Set to false to disable script
        font: 'Contrail One',       // Choices: 'Arial', 'Patrick Hand', 'Contrail One', 'Shadows Into Light', 'Candal'
        damageColor: 'black',       // Color of damage numbers
        healingColor: 'green',      // Color of healing numbers
        healthBar: 1,               // Which bar is the health bar?
        displayHealing: true,       // Show green numbers when health increases
        requireMaxHealth: false,    // Only show damage numbers if the token has a maximum health
        requirePlayerPage: true     // Only show damage numbers on the active player page
    };


    const barValue = `bar${config.healthBar}_value`;
    const barMax = `bar${config.healthBar}_max`;

    const getActivePages = () => [
        ...new Set([
            Campaign().get('playerpageid'),
            ...Object.values(Campaign().get('playerspecificpages')),
            ...findObjs({
                type: 'player',
                online: true
            })
            .filter((p)=>playerIsGM(p.id))
            .map((p)=>p.get('lastpage'))
        ])
    ];

    // Generate damage/healing numbers
    on('change:graphic', function(obj, prev) {
        if(config.enabled) {
            if(config.requirePlayerPage) {
                // Do nothing if it was a token on another page
                let activePages = getActivePages();

                if(!activePages.includes(obj.get('_pageid'))) {
                    return;
                }
            }

            const oldHp = parseInt(prev[barValue]);
            const oldHpMax = parseInt(prev[barMax]);
            const newHp = parseInt(obj.get(barValue));
            const newHpMax = parseInt(obj.get(barMax));
            
            // Do nothing if the bar value didn't change
            if(oldHp && newHp && oldHp == newHp) {
                return;
            }
            
            
            damageNumber(obj, oldHp, newHp, oldHpMax, newHpMax);
        }
    });

    // Display damage number for a token
    function damageNumber(token, oldHp, newHp, oldHpMax, newHpMax) {
        if(oldHp != oldHp || newHp != newHp || 
            (oldHpMax == oldHpMax && newHpMax == newHpMax && oldHpMax != newHpMax) || 
            oldHp - newHp == oldHpMax - newHpMax) {
            // NaN values or max HP changed, don't show a number
            return;
        }

        let hpChange = newHp - oldHp;

        if(!config.displayHealing && hpChange > 0) {
            // Do nothing if it's a healing value and healing numbers are disabled
            return;
        }

        if(config.requireMaxHealth && !newHpMax) {
            // Do nothing if there's no max HP and require max health is enabled
            return;
        }

        let number = createObj('text', {
            _pageid: token.get('_pageid'),
            layer: token.get('layer'),
            left: token.get('left') - token.get('width') * 0.4 + Math.floor(Math.random() * (token.get('width') * 0.8)),
            top: token.get('top') - token.get('height') / 2 + Math.floor(Math.random() * 20),
            text: Math.abs(hpChange).toString(),
            width: 50,
            height: 25.6,
            font_family: config.font,
            color: hpChange > 0 ? config.healingColor : config.damageColor
        });

        log(`Created damage number for ${Math.abs(hpChange)} ${hpChange > 0 ? 'healing' : 'damage'} to token ${token.get('name') ? token.get('name') : token.get('_id')}.`);

        updateDamageNumber(number, number.get('top') - 50, 20);
    }

    // Move the number upwards slightly every 50ms, then delete it
    function updateDamageNumber(number, targetTop, steps) {
        if(steps <= 0) {
            number.remove();
            return;
        }

        let top = number.get('top');
        top += (targetTop - top) * 0.3;
        number.set('top', top);
        setTimeout(function () {
            updateDamageNumber(number, targetTop, steps - 1);
        }, 50);
    }
});




April 05 (6 years ago)
Spren
Sheet Author

Nice Aaron!

Prof, I'd love an option at the top to increase the font size a tiny bit. It looks great as is but in some cases it's hard to read.

April 05 (6 years ago)
Quinn
Pro
Sheet Author

Thanks for the tips! I had previously considered and discarded the idea of building the bar property names in advance because I forgot that `prev.bar1_value` can also be written as `prev['bar1_value']`.

I'll look over the rest in more detail later and push a new version.

April 05 (6 years ago)
The Aaron
Roll20 Production Team
API Scripter

Yeah, “array notation” is what that’s called. Super handy for building dynamic code. 

Another enhancement you might consider is creating two copies of the text, one in white and one in black (or some other pair of contrasting colors), and have the white offset a few pixels in each axis. If you make the white one first it will be below the black and give a nice contrast to the edge to let it show up better on dark backgrounds. Then you can just pass an array of objects to adjust to your updateDamageNumber function. 

April 05 (6 years ago)

Edited April 05 (6 years ago)
Quinn
Pro
Sheet Author

All right! I've updated the script at the same old place - code refactored, and I added a config flag to adjust font size.

e: ha! You made that suggestion while I was making my changes. I'll get it in soon.

April 05 (6 years ago)

I really like this script...great work...
I use token-mod to apply damage sometimes...
Is there a way to use FF with token-mod (apply damage macro)...???
Don't want to ask too much....

April 05 (6 years ago)
GM Michael
API Scripter

Awesome!  How did this not exist already?

April 05 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

Can I ask what the purpose of the random element in these lines is?

            left: token.get('left') - token.get('width') * 0.4 + Math.floor(Math.random() * (token.get('width') * 0.8)),
            top: token.get('top') - token.get('height') / 2 + Math.floor(Math.random() * 20),


April 05 (6 years ago)
The Aaron
Roll20 Production Team
API Scripter

GiGs: it will adjust the starting position of the text at a random point in the conceptual box just above the token. Probably to more closely match how the numbers in many JRPG games look, and to give a more organic aesthetic.  

April 05 (6 years ago)
The Aaron
Roll20 Production Team
API Scripter


BilBo 2 said:

I really like this script...great work...
I use token-mod to apply damage sometimes...
Is there a way to use FF with token-mod (apply damage macro)...???
Don't want to ask too much....


That’s definitely doable, I was going to post the necessary code for that (and ApplyDamage) when I got home today... which is in about 10 minutes. =D

April 05 (6 years ago)
The Aaron
Roll20 Production Team
API Scripter

Ok, handling TokenMod and ApplyDamage changes is as easy as registering your function with those scripts if they are installed:

First, you move your event handler for 'change:graphic' from being an anonymous function argument, to being it's own function, something like:

    // Generate damage/healing numbers
    const onTokenChange = (obj, prev) => {
           /* ... */
    };
Then you pass it as argument to the on 'change:graphic' registration:
	on('change:graphic', onTokenChange);
Then you check for the existence of the other scripts and their registration functions, and call them also with that function:
    if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){
        TokenMod.ObserveTokenChange(onTokenChange);
    }
    if('undefined' !== typeof ApplyDamage && ApplyDamage.registerObserver){
        ApplyDamage.registerObserver("change", onTokenChange);
    }

Easy-peasy.




Here's what it looks like integrated into the current version:

/* Damage Numbers script
* Generates Final Fantasy-style damage numbers whenever a token's health increases or decreases.
* Works for any system.
* 
* Created by Quinn Gordon
* Roll20: https://app.roll20.net/users/42042/prof
* Github: https://github.com/ProfessorProf
*/

on('ready', () => {
    // Config:
    const config = {
        enabled: true,              // Set to false to disable script
        font: 'Contrail One',       // Choices: 'Arial', 'Patrick Hand', 'Contrail One', 'Shadows Into Light', 'Candal'
        fontSize: 16,               // Font size of damage numbers
        damageColor: 'black',       // Color of damage numbers
        healingColor: 'green',      // Color of healing numbers
        healthBar: 1,               // Which bar is the health bar?
        displayHealing: true,       // Show green numbers when health increases
        requireMaxHealth: false,    // Only show damage numbers if the token has a maximum health
        requirePlayerPage: true     // Only show damage numbers on the active player page
    };
    
    const barValueKey = `bar${config.healthBar}_value`;
    const barMaxKey = `bar${config.healthBar}_max`;
    
    const getActivePages = () => [
        ...new Set([
            Campaign().get('playerpageid'),
            ...Object.values(Campaign().get('playerspecificpages')),
            ...findObjs({
                type: 'player',
                online: true
            })
            .filter((p)=>playerIsGM(p.id))
            .map((p)=>p.get('lastpage'))
        ])
    ];
    
    // Generate damage/healing numbers
    const onTokenChange = (obj, prev) => {
        if(config.enabled) {
            if(config.requirePlayerPage) {
                // Do nothing if it was a token on another page
                let activePages = getActivePages();

                if(!activePages.includes(obj.get('_pageid'))) {
                    return;
                }
            }
            
            const oldHp = parseInt(prev[barValueKey]);
            const oldHpMax = parseInt(prev[barMaxKey]);
            const newHp = parseInt(obj.get(barValueKey));
            const newHpMax = parseInt(obj.get(barMaxKey));
            
            // Do nothing if the bar value didn't change
            if(oldHp && newHp && oldHp == newHp) {
                return;
            }
            
            damageNumber(obj, oldHp, newHp, oldHpMax, newHpMax);
        }
    };
    
    // Display damage number for a token
    function damageNumber(token, oldHp, newHp, oldHpMax, newHpMax) {
        if(oldHp != oldHp || newHp != newHp || 
            (oldHpMax == oldHpMax && newHpMax == newHpMax && oldHpMax != newHpMax) || 
            oldHp - newHp == oldHpMax - newHpMax) {
            // NaN values or max HP changed, don't show a number
            return;
        }
    
        let hpChange = newHp - oldHp;
    
        if(!config.displayHealing && hpChange > 0) {
            // Do nothing if it's a healing value and healing numbers are disabled
            return;
        }
    
        if(config.requireMaxHealth && !newHpMax) {
            // Do nothing if there's no max HP and require max health is enabled
            return;
        }
    
        let number = createObj('text', {
            _pageid: token.get('_pageid'),
            layer: token.get('layer'),
            left: token.get('left') - token.get('width') * 0.4 + Math.floor(Math.random() * (token.get('width') * 0.8)),
            top: token.get('top') - token.get('height') / 2 + Math.floor(Math.random() * 20),
            text: Math.abs(hpChange).toString(),
            font_family: config.font,
            font_size: config.fontSize,
            color: hpChange > 0 ? config.healingColor : config.damageColor
        });
    
        log(`Created damage number for ${Math.abs(hpChange)} ${hpChange > 0 ? 'healing' : 'damage'} to token ${token.get('name') ? token.get('name') : token.get('_id')}.`);
    
        updateDamageNumber(number, number.get('top') - 50, 20);
    }
    
    // Move the number upwards slightly every 50ms, then delete it
    function updateDamageNumber(number, targetTop, steps) {
        if(steps <= 0) {
            number.remove();
            return;
        }
    
        let top = number.get('top');
        top += (targetTop - top) * 0.3;
        number.set('top', top);
        setTimeout(function () {
            updateDamageNumber(number, targetTop, steps - 1);
        }, 50);
    }


	on('change:graphic', onTokenChange);
    if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){
        TokenMod.ObserveTokenChange(onTokenChange);
    }
    if('undefined' !== typeof ApplyDamage && ApplyDamage.registerObserver){
        ApplyDamage.registerObserver("change", onTokenChange);
    }

});

April 05 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter


The Aaron said:

GiGs: it will adjust the starting position of the text at a random point in the conceptual box just above the token. Probably to more closely match how the numbers in many JRPG games look, and to give a more organic aesthetic.  

I could tell what it did, i just didnt understand why it was doing it. I've never played those games, that makes sense, thanks.

April 05 (6 years ago)

easy paeasy.....for you to say.... :)

Works...sweet...

April 05 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

Well, scripting comes so easily to Aaron that he writes these scripts while gaming, so yeah :)

Here's the script, reformatted with the above, though I'm gaming right now, so I haven't tested it. =D

Now, I know he probably meant table top rpg, but I'm enjoying a mental image of some hacker-like scene where he's playing doom or a jrpg on one screen with one hand on one keyboard, and his other hand on another keyboard writing up his script on another screen as matrix-style text scrolls down it...

April 05 (6 years ago)

you had to plant that image.....thanks....

April 05 (6 years ago)
The Aaron
Roll20 Production Team
API Scripter

I don't even see the code anymore, all I see is blonde, brunette, redhead...

It was Tabletop Gaming (well, Roll20), but I did drink 750ml of red wine and narrowly avert a TPK whilst doing it... =D

April 05 (6 years ago)
GM Michael
API Scripter

Now that I'm home and can test this, my only request is that font scale with token size.  On larger enemies, this seems a bit...  anticlimactic.

April 06 (6 years ago)
Ravenknight
KS Backer

So good! Using this tonight. :)

April 06 (6 years ago)

Edited April 06 (6 years ago)
GM Michael
API Scripter

Since my party is about to hit the below zone boss tomorrow, I added support for scaling the font size based on token size and major damage (currently >= 50% hp change yields 3x font size).


/* Damage Numbers script
* Generates Final Fantasy-style damage numbers whenever a token's health increases or decreases.
* Works for any system.
*
* Created by Quinn Gordon
* Roll20: https://app.roll20.net/users/42042/prof
* Github: https://github.com/ProfessorProf
*/

on('ready', () => {
// Config:
const config = {
enabled: true, // Set to false to disable script
font: 'Contrail One', // Choices: 'Arial', 'Patrick Hand', 'Contrail One', 'Shadows Into Light', 'Candal'
fontSize: 16, // Font size of damage numbers
damageColor: 'red', // Color of damage numbers
healingColor: 'blue', // Color of healing numbers
healthBar: 1, // Which bar is the health bar?
displayHealing: true, // Show green numbers when health increases
requireMaxHealth: false, // Only show damage numbers if the token has a maximum health
requirePlayerPage: true, // Only show damage numbers on the active player page
tokenSizeScaling: true, // Enable font size scaling based on token size
majorDamageScaling: true // Enable font size scaling based on damage dealt (>50% hp = 3x bigger)
};
const barValueKey = `bar${config.healthBar}_value`;
const barMaxKey = `bar${config.healthBar}_max`;
const getActivePages = () => [
...new Set([
Campaign().get('playerpageid'),
...Object.values(Campaign().get('playerspecificpages')),
...findObjs({
type: 'player',
online: true
})
.filter((p)=>playerIsGM(p.id))
.map((p)=>p.get('lastpage'))
])
];
// Generate damage/healing numbers
const onTokenChange = (obj, prev) => {
if(config.enabled) {
if(config.requirePlayerPage) {
// Do nothing if it was a token on another page
let activePages = getActivePages();

if(!activePages.includes(obj.get('_pageid'))) {
return;
}
}
const oldHp = parseInt(prev[barValueKey]);
const oldHpMax = parseInt(prev[barMaxKey]);
const newHp = parseInt(obj.get(barValueKey));
const newHpMax = parseInt(obj.get(barMaxKey));
// Do nothing if the bar value didn't change
if(oldHp && newHp && oldHp == newHp) {
return;
}
damageNumber(obj, oldHp, newHp, oldHpMax, newHpMax);
}
};
// Display damage number for a token
function damageNumber(token, oldHp, newHp, oldHpMax, newHpMax) {
if(oldHp != oldHp || newHp != newHp ||
(oldHpMax == oldHpMax && newHpMax == newHpMax && oldHpMax != newHpMax) ||
oldHp - newHp == oldHpMax - newHpMax) {
// NaN values or max HP changed, don't show a number
return;
}
let hpChange = newHp - oldHp;
if(!config.displayHealing && hpChange > 0) {
// Do nothing if it's a healing value and healing numbers are disabled
return;
}
if(config.requireMaxHealth && !newHpMax) {
// Do nothing if there's no max HP and require max health is enabled
return;
}

let width = token.get('width');
let height = token.get('height');
let fontSize = scaleFont(height, width, hpChange, newHpMax);
// Create number at random location at the top of the token
let number = createObj('text', {
_pageid: token.get('_pageid'),
layer: token.get('layer'),
left: token.get('left') - width * 0.4 + Math.floor(Math.random() * (width * 0.8)),
top: token.get('top') - height / 2 + Math.floor(Math.random() * 20),
text: Math.abs(hpChange).toString(),
font_family: config.font,
font_size: fontSize,
color: hpChange > 0 ? config.healingColor : config.damageColor
});
log(`Created damage number for ${Math.abs(hpChange)} ${hpChange > 0 ? 'healing' : 'damage'} to token ${token.get('name') ? token.get('name') : token.get('_id')}.`);
updateDamageNumber(number, number.get('top') - 50, 20);
}

// Scale font size based on token size and damage dealt
function scaleFont(height, width, hpChange, hpMax){
// Roll20's pixels per unit on the map
const pxPerUnit = 70;

let scaledFont = config.fontSize;

// Scale based on token size
if (config.tokenSizeScaling) {
// Get average token dimension for font scaling
let tokenScale = (height + width) / (2 * pxPerUnit);
scaledFont *= Math.max(tokenScale, 1);
}

// Scale based on damage dealt
if (config.majorDamageScaling && Math.abs(hpChange) * 2 > hpMax) {
scaledFont *= 3;
}
return scaledFont;
}
// Move the number upwards slightly every 50ms, then delete it
function updateDamageNumber(number, targetTop, steps) {
if(steps <= 0) {
number.remove();
return;
}
let top = number.get('top');
top += (targetTop - top) * 0.3;
number.set('top', top);
setTimeout(function () {
updateDamageNumber(number, targetTop, steps - 1);
}, 50);
}


    on('change:graphic', onTokenChange);
if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){
TokenMod.ObserveTokenChange(onTokenChange);
}
if('undefined' !== typeof ApplyDamage && ApplyDamage.registerObserver){
ApplyDamage.registerObserver("change", onTokenChange);
}

});