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

1554304439
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
1554304879
Spren
Sheet Author
I love it. Adding it to my game. 
1554305940

Edited 1554305969
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
That's neat! Cool idea.
Very Cool! Thanks for sharing!
1554350002
Ziechael
Forum Champion
Sheet Author
API Scripter
Nice... visual aids are always great!
1554400192
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?
1554407131
Quinn
Pro
Sheet Author
Sure! I'm always happy to learn how to refine my craft.
1554409295
The Aaron
Roll20 Production Team
API Scripter
Cool. I’ll get you some feedback with those functions. =D
1554411733
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).
1554429501

Edited 1554430153
The Aaron
Roll20 Production Team
API Scripter
Yup, that was one of the things I was going to mention.&nbsp; =D Like GiGs pointed out, you can reduce code duplication by building the property names based on the healthBar configuration.&nbsp; 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.&nbsp; That's just personal preference, but it makes reordering and moving things simple.) Another thing to be aware of is the 'ready' event.&nbsp; When the API starts up, it sends an add event for every object in the entire game.&nbsp; 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.&nbsp; To get around that, you wrap your script in: on('ready',()=&gt;{ /* 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:&nbsp; All scripts in the API are concatenated into a single file, which means there is a single global namespace.&nbsp; That means any functions you put in the global namespace could collide with other functions.&nbsp; 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.&nbsp; I notice you're checking the player page if requirePlayerPage is set.&nbsp; There are technically 3 possible settings for pages.&nbsp; There's the playerpageid, the playerspecificpages, and the lastpage on a player.&nbsp; I wrote a function that builds the right list: const getActivePages = () =&gt; [ ...new Set([ Campaign().get('playerpageid'), ...Object.values(Campaign().get('playerspecificpages')), ...findObjs({ type: 'player', online: true }) .filter((p)=&gt;playerIsGM(p.id)) .map((p)=&gt;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.&nbsp; The state object is intended to store values that are persisted across loads of the api.&nbsp; It's ideal for settings, but ONLY if there is a way to set them at some point.&nbsp; To paraphrase The King of Attolia by Megan Whalen Turner&nbsp; (which is a great series) when you constantly overwrite it on load " you thwart its purpose ."&nbsp; 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.&nbsp; I wrote a whole section on this in the wiki:&nbsp; <a href="https://wiki.roll20.net/API:Objects#state" rel="nofollow">https://wiki.roll20.net/API:Objects#state</a> 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.&nbsp; 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: <a href="https://app.roll20.net/users/42042/prof" rel="nofollow">https://app.roll20.net/users/42042/prof</a> * Github: <a href="https://github.com/ProfessorProf" rel="nofollow">https://github.com/ProfessorProf</a> */ on('ready', ()=&gt;{ // 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 = () =&gt; [ ...new Set([ Campaign().get('playerpageid'), ...Object.values(Campaign().get('playerspecificpages')), ...findObjs({ type: 'player', online: true }) .filter((p)=&gt;playerIsGM(p.id)) .map((p)=&gt;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 &amp;&amp; newHp &amp;&amp; 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 &amp;&amp; newHpMax == newHpMax &amp;&amp; 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 &amp;&amp; hpChange &gt; 0) { // Do nothing if it's a healing value and healing numbers are disabled return; } if(config.requireMaxHealth &amp;&amp; !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 &gt; 0 ? config.healingColor : config.damageColor }); log(`Created damage number for ${Math.abs(hpChange)} ${hpChange &gt; 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 &lt;= 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); } });
1554465370
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.
1554468604
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.
1554471878
The Aaron
Roll20 Production Team
API Scripter
Yeah, “array notation” is what that’s called. Super handy for building dynamic code.&nbsp; 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.&nbsp;
1554472343

Edited 1554472394
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.
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....
Awesome!&nbsp; How did this not exist already?
1554479149
GiGs
Pro
Sheet Author
API Scripter
Can I ask what the purpose of the random element in these lines is? &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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),
1554485650
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. &nbsp;
1554485720
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
1554487421
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) =&gt; { /* ... */ }; 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 &amp;&amp; TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(onTokenChange); } if('undefined' !== typeof ApplyDamage &amp;&amp; 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: <a href="https://app.roll20.net/users/42042/prof" rel="nofollow">https://app.roll20.net/users/42042/prof</a> * Github: <a href="https://github.com/ProfessorProf" rel="nofollow">https://github.com/ProfessorProf</a> */ on('ready', () =&gt; { // 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 = () =&gt; [ ...new Set([ Campaign().get('playerpageid'), ...Object.values(Campaign().get('playerspecificpages')), ...findObjs({ type: 'player', online: true }) .filter((p)=&gt;playerIsGM(p.id)) .map((p)=&gt;p.get('lastpage')) ]) ]; // Generate damage/healing numbers const onTokenChange = (obj, prev) =&gt; { 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 &amp;&amp; newHp &amp;&amp; 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 &amp;&amp; newHpMax == newHpMax &amp;&amp; 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 &amp;&amp; hpChange &gt; 0) { // Do nothing if it's a healing value and healing numbers are disabled return; } if(config.requireMaxHealth &amp;&amp; !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 &gt; 0 ? config.healingColor : config.damageColor }); log(`Created damage number for ${Math.abs(hpChange)} ${hpChange &gt; 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 &lt;= 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 &amp;&amp; TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(onTokenChange); } if('undefined' !== typeof ApplyDamage &amp;&amp; ApplyDamage.registerObserver){ ApplyDamage.registerObserver("change", onTokenChange); } });
1554487783
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. &nbsp; I could tell what it did, i just didnt understand why&nbsp;it was doing it. I've never played those games, that makes sense, thanks.
easy paeasy.....for you to say.... :) Works...sweet...
1554488532
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...
you had to plant that image.....thanks....
1554490111
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
Now that I'm home and can test this, my only request is that font scale with token size.&nbsp; On larger enemies, this seems a bit...&nbsp; anticlimactic.
So good! Using this tonight. :)
1554559224

Edited 1554561559
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 &gt;= 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: <a href="https://app.roll20.net/users/42042/prof" rel="nofollow">https://app.roll20.net/users/42042/prof</a> * Github: <a href="https://github.com/ProfessorProf" rel="nofollow">https://github.com/ProfessorProf</a> */ on ( 'ready' , () =&gt; { // 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 (&gt;50% hp = 3x bigger) }; const barValueKey = `bar ${ config . healthBar } _value` ; const barMaxKey = `bar ${ config . healthBar } _max` ; const getActivePages = () =&gt; [ ... new Set ([ Campaign (). get ( 'playerpageid' ), ... Object . values ( Campaign (). get ( 'playerspecificpages' )), ... findObjs ({ type : 'player' , online : true }) . filter (( p ) =&gt; playerIsGM ( p . id )) . map (( p ) =&gt; p . get ( 'lastpage' )) ]) ]; // Generate damage/healing numbers const onTokenChange = ( obj , prev ) =&gt; { 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 &amp;&amp; newHp &amp;&amp; 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 &amp;&amp; newHpMax == newHpMax &amp;&amp; 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 &amp;&amp; hpChange &gt; 0 ) { // Do nothing if it's a healing value and healing numbers are disabled return ; } if ( config . requireMaxHealth &amp;&amp; ! 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 &gt; 0 ? config . healingColor : config . damageColor }); log ( `Created damage number for ${ Math . abs ( hpChange ) } ${ hpChange &gt; 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 &amp;&amp; Math . abs ( hpChange ) * 2 &gt; hpMax ) { scaledFont *= 3 ; } return scaledFont ; } // Move the number upwards slightly every 50ms, then delete it function updateDamageNumber ( number , targetTop , steps ) { if ( steps &lt;= 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 ); } &nbsp;&nbsp;&nbsp;&nbsp; on ( 'change:graphic' , onTokenChange ); if ( 'undefined' !== typeof TokenMod &amp;&amp; TokenMod . ObserveTokenChange ){ TokenMod . ObserveTokenChange ( onTokenChange ); } if ( 'undefined' !== typeof ApplyDamage &amp;&amp; ApplyDamage . registerObserver ){ ApplyDamage . registerObserver ( "change" , onTokenChange ); } });