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 .
×

[Help] A script to check if a player has taken more than half their hp max in one hit

Hi all, I'm trying to write a script to automate a major injuries mechanic in my 5e campaigns (This is the Dael Kingsmill major injuries system, where if a player takes more than half of their maximum hp in a single hit they must make a constitution save to avoid taking a major injury). I don't need the code to call the tables or make the rolls or anything, just to check if the conditions are met and send a message to the chat stating that a major injury roll is needed. I've tried to do this by piggybacking off Robin Kuiper's DeathTracker code as it has some similar functionality for tracking massive damage, but after my edits it returns an Unexpected Identifier syntax error and I can't figure out where for the death of me. If anyone could give me a pointer, or alternatively suggest a simpler way to add this functionality I would be very appreciative! Link to my code : <a href="https://gist.github.com/calnevs/24b9cd30a6f53168807fbd501d29d5bf" rel="nofollow">https://gist.github.com/calnevs/24b9cd30a6f53168807fbd501d29d5bf</a> Link to Robin Kuiper's original code : <a href="https://github.com/RobinKuiper/Roll20APIScripts/blob/master/DeathTracker/DeathTracker.js" rel="nofollow">https://github.com/RobinKuiper/Roll20APIScripts/blob/master/DeathTracker/DeathTracker.js</a>
1633282797
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
That means you've got something wrong with your syntax. Run your code through a JSlinter, like the google closure compiler or JSLint &nbsp;to find the issue. For things like this I actually prefer the closure compiler even though it's less powerful because it ignores style issues and only returns errors for actual breaking issues.
1633295783
The Aaron
Roll20 Production Team
API Scripter
Line 330 is missing the , after: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; heal_fx_type: 'glow-holy' Here's a version with that and some linting issues fixed: /* globals TokenMod */ const DeathTracker = (function () { // eslint-disable-line no-unused-vars 'use strict'; let observers = { tokenChange: [] }; // Styling for the chat responses. const styles = { reset: 'padding: 0; margin: 0;', menu: 'background-color: #fff; border: 1px solid #000; padding: 5px; border-radius: 5px;', button: 'background-color: #000; border: 1px solid #292929; border-radius: 3px; padding: 5px; color: #fff; text-align: center;', list: 'list-style: none;', float: { right: 'float: right;', left: 'float: left;' }, overflow: 'overflow: hidden;', fullWidth: 'width: 100%;', underline: 'text-decoration: underline;', strikethrough: 'text-decoration: strikethrough' }, script_name = 'DeathTracker', state_name = 'DEATHTRACKER', markers = ['none', 'blue', 'brown', 'green', 'pink', 'purple', 'red', 'yellow', '-', 'all-for-one', 'angel-outfit', 'archery-target', 'arrowed', 'aura', 'back-pain', 'black-flag', 'bleeding-eye', 'bolt-shield', 'broken-heart', 'broken-shield', 'broken-skull', 'chained-heart', 'chemical-bolt', 'cobweb', 'dead', 'death-zone', 'drink-me', 'edge-crack', 'fishing-net', 'fist', 'fluffy-wing', 'flying-flag', 'frozen-orb', 'grab', 'grenade', 'half-haze', 'half-heart', 'interdiction', 'lightning-helix', 'ninja-mask', 'overdrive', 'padlock', 'pummeled', 'radioactive', 'rolling-tomb', 'screaming', 'sentry-gun', 'skull', 'sleepy', 'snail', 'spanner', 'stopwatch', 'strong', 'three-leaves', 'tread', 'trophy', 'white-tower'], handleInput = (msg) =&gt; { if (msg.type != 'api' || !playerIsGM(msg.playerid)) return; // Split the message into command and argument(s) let args = msg.content.split(' '); let command = args.shift().substring(1); let extracommand = args.shift(); if (command == state[state_name].config.command) { switch (extracommand) { case 'reset': state[state_name] = {}; setDefaults(true); sendConfigMenu(); break; case 'config': { let message; if (args.length &gt; 0) { let setting = args.shift().split('|'); let key = setting.shift(); let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; if (key === 'death_statusmarker') { if (value !== state[state_name].config.half_statusmarker) { state[state_name].config[key] = value; } else { message = '&lt;span style="color: red"&gt;Statusmakers can\'t be the same.&lt;/span&gt;'; } } else if (key === 'half_statusmarker') { if (value !== state[state_name].config.death_statusmarker) { state[state_name].config[key] = value; } else { message = '&lt;span style="color: red"&gt;Statusmakers can\'t be the same.&lt;/span&gt;'; } } else { state[state_name].config[key] = value; } if (key === 'bar') { //registerEventHandlers(); message = '&lt;span style="color: red"&gt;The API Sandbox needs to be restarted for this to take effect.&lt;/span&gt;'; } } sendConfigMenu(false, message); } break; default: sendConfigMenu(); break; } } }, handleBarValueChange = (obj, prev) =&gt; { let bar = 'bar' + state[state_name].config.bar; if (!obj || !prev || !obj.get('represents') || obj.get(bar + '_value') === prev[bar + '_value']) { return; // Don't continue if values are the same, or token is not a character. } // Get current and prev hp, and calc damage taken. let currentHP = parseInt(obj.get(bar + '_value')), prevCurrentHP = parseInt(prev[bar + '_value']), maxHP = parseInt(obj.get(bar + '_max')), damageTaken = prevCurrentHP - currentHP; let attributes = {}; // Get marker settings. let deathMarker = state[state_name].config.death_statusmarker, halfMarker = state[state_name].config.half_statusmarker, majorInjury = state[state_name].config.injury_statusmarker, unconsciousMarker = state[state_name].config.pc_unconscious_statusmarker; // Get playerid let playerid = (obj.get('controlledby') &amp;&amp; obj.get('controlledby') !== '') ? obj.get('controlledby') : (getObj('character', obj.get('represents'))) ? getObj('character', obj.get('represents')).get('controlledby') : false; // Check if it is a player. let isPlayer = (playerid &amp;&amp; !playerIsGM(playerid)); let marker; // If no death marker is set yet, and target becomes below 0 hp, add marker. if (deathMarker !== 'none' &amp;&amp; currentHP &lt;= 0) { if(!isPlayer){ marker = deathMarker; }else if(state[state_name].config.massive_damage &amp;&amp; damageTaken &gt; prevCurrentHP &amp;&amp; (damageTaken - prevCurrentHP) &gt; maxHP){ // MASSIVE DAMAGE marker = deathMarker; makeAndSendMenu(obj.get('name') + ' took massive damage, and instantly died.', '', ''); }else{ marker = (unconsciousMarker !== 'none') ? unconsciousMarker : deathMarker; } attributes['status_' + marker] = true; attributes['status_' + halfMarker] = false; // Remove halfmarker. } else { // If hp is above 0, remove markers, check for halfmarker. attributes['status_' + deathMarker] = false; attributes['status_' + unconsciousMarker] = false; attributes['status_' + halfMarker] = (halfMarker !== 'none' &amp;&amp; maxHP !== '' &amp;&amp; currentHP &lt;= maxHP / 2); } //Major Injury Notifier if(state[state_name].config.major_injury &amp;&amp; damageTaken &gt; (maxHP / 2) &amp;&amp; currentHP &lt; prevCurrentHP &amp;&amp; prevCurrentHP &gt; 0){ marker = majorInjury; makeAndSendMenu(obj.get('name') + ' suffered a major injury.', '', ''); } // Do tints if(state[state_name].config.change_player_tint &amp;&amp; isPlayer || state[state_name].config.change_npc_tint &amp;&amp; !isPlayer){ let color = getColor(1 - (currentHP / maxHP)); attributes.tint_color = (maxHP == currentHP) ? 'transparent' : color; } // Do damage fx if(state[state_name].config.fx &amp;&amp; currentHP &lt; parseInt(prev[bar + '_value'])){ let x = parseInt(obj.get('left')), y = parseInt(obj.get('top')); spawnFxBetweenPoints({ x, y }, { x, y }, state[state_name].config.fx_type, obj.get('pageid')); } // Do heal fx if(state[state_name].config.heal_fx &amp;&amp; currentHP &gt; parseInt(prev[bar + '_value'])){ let x = parseInt(obj.get('left')), y = parseInt(obj.get('top')); spawnFxBetweenPoints({ x, y }, { x, y }, state[state_name].config.heal_fx_type, obj.get('pageid')); } obj.set(attributes); notifyObservers('tokenChange', obj, prev); }, getColor = (value) =&gt; { return hslToHex(((1-value)*120), 75, 50); }, hslToHex = (h, s, l) =&gt; { h /= 360; s /= 100; l /= 100; let r, g, b; if (s === 0) { r = g = b = l; // achromatic } else { const hue2rgb = (p, q, t) =&gt; { if (t &lt; 0) t += 1; if (t &gt; 1) t -= 1; if (t &lt; 1 / 6) return p + (q - p) * 6 * t; if (t &lt; 1 / 2) return q; if (t &lt; 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l &lt; 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } const toHex = x =&gt; { const hex = Math.round(x * 255).toString(16); return hex.length === 1 ? '0' + hex : hex; }; return `#${toHex(r)}${toHex(g)}${toHex(b)}`; }, ucFirst = (string) =&gt; { return string.charAt(0).toUpperCase() + string.slice(1); }, sendConfigMenu = (first, message) =&gt; { let markerDropdown = '?{Marker'; markers.forEach((marker) =&gt; { markerDropdown += '|' + ucFirst(marker).replace('-', ' ') + ',' + marker; }); markerDropdown += '}'; let massiveDmgButton = makeButton(state[state_name].config.massive_damage, '!' + state[state_name].config.command + ' config massive_damage|' + !state[state_name].config.massive_damage, styles.button + styles.float.right), death_markerButton = makeButton(state[state_name].config.death_statusmarker, '!' + state[state_name].config.command + ' config death_statusmarker|' + markerDropdown, styles.button + styles.float.right), half_markerButton = makeButton(state[state_name].config.half_statusmarker, '!' + state[state_name].config.command + ' config half_statusmarker|' + markerDropdown, styles.button + styles.float.right), commandButton = makeButton('!' + state[state_name].config.command, '!' + state[state_name].config.command + ' config command|?{Command (without !)}', styles.button + styles.float.right), barButton = makeButton('bar ' + state[state_name].config.bar, '!' + state[state_name].config.command + ' config bar|?{Bar|Bar 1 (green),1|Bar 2 (blue),2|Bar 3 (red),3}', styles.button + styles.float.right), pc_unconscious_markerButton = makeButton(state[state_name].config.pc_unconscious_statusmarker, '!' + state[state_name].config.command + ' config pc_unconscious_statusmarker|' + markerDropdown, styles.button + styles.float.right), change_player_tintButton = makeButton(state[state_name].config.change_player_tint, '!' + state[state_name].config.command + ' config change_player_tint|' + !state[state_name].config.change_player_tint, styles.button + styles.float.right), change_npc_tintButton = makeButton(state[state_name].config.change_npc_tint, '!' + state[state_name].config.command + ' config change_npc_tint|' + !state[state_name].config.change_npc_tint, styles.button + styles.float.right), fxButton = makeButton(state[state_name].config.fx, '!' + state[state_name].config.command + ' config fx|' + !state[state_name].config.fx, styles.button + styles.float.right), fx_typeButton = makeButton(state[state_name].config.fx_type, '!' + state[state_name].config.command + ' config fx_type|?{FX Type|'+state[state_name].config.fx_type+'}', styles.button + styles.float.right), heal_fxButton = makeButton(state[state_name].config.heal_fx, '!' + state[state_name].config.command + ' config heal_fx|' + !state[state_name].config.heal_fx, styles.button + styles.float.right), heal_fx_typeButton = makeButton(state[state_name].config.heal_fx_type, '!' + state[state_name].config.command + ' config heal_fx_type|?{Heal FX Type|'+state[state_name].config.heal_fx_type+'}', styles.button + styles.float.right), majorInjuryButton = makeButton(state[state_name].config.major_injury, '!' + state[state_name].config.command + ' config major_injury| ' + !state[state_name].config.massive_damage, styles.button + styles.float.right), injury_markerButton = makeButton(state[state_name].config.injury_statusmarker, '!' + state[state_name].config.command + ' config injury_statusmarker|' + markerDropdown, styles.button + styles.float.right), listItems = [ '&lt;span style="'+styles.float.left+'"&gt;Command:&lt;/span&gt; ' + commandButton, '&lt;span style="'+styles.float.left+'"&gt;Massive Damage:&lt;/span&gt; ' + massiveDmgButton, '&lt;span style="'+styles.float.left+'"&gt;HP Bar:&lt;/span&gt; ' + barButton, '&lt;span style="'+styles.float.left+'"&gt;Dead Statusmarker:&lt;/span&gt; ' + death_markerButton, '&lt;span style="'+styles.float.left+'"&gt;Uncon. Statusmarker:&lt;div style="font-size: 8pt"&gt;Unconscious marker if PC.&lt;/div&gt;&lt;/span&gt; ' + pc_unconscious_markerButton, '&lt;span style="'+styles.float.left+'"&gt;Half Dead Statusmarker:&lt;/span&gt; ' + half_markerButton, '&lt;span style="'+styles.float.left+'"&gt;Change Player Tint Color:&lt;/span&gt; ' + change_player_tintButton, '&lt;span style="'+styles.float.left+'"&gt;Change NPC Tint Color:&lt;/span&gt; ' + change_npc_tintButton, '&lt;span style="'+styles.float.left+'"&gt;Use FX:&lt;div style="font-size: 8pt"&gt;When getting damage.&lt;/div&gt;&lt;/span&gt; ' + fxButton, '&lt;span style="'+styles.float.left+'"&gt;Major Injury:&lt;/span&gt; ' + majorInjuryButton, '&lt;span style="'+styles.float.left+'"&gt;Major Injury Statusmarker:&lt;/span&gt; ' + injury_markerButton ]; if(state[state_name].config.fx){ listItems.push('&lt;span style="'+styles.float.left+'"&gt;FX Type:&lt;/span&gt; ' + fx_typeButton); } listItems.push('&lt;span style="'+styles.float.left+'"&gt;Use Heal FX:&lt;/span&gt; ' + heal_fxButton); if(state[state_name].config.heal_fx){ listItems.push('&lt;span style="'+styles.float.left+'"&gt;Heal FX Type:&lt;/span&gt; ' + heal_fx_typeButton); } let resetButton = makeButton('Reset', '!' + state[state_name].config.command + ' reset', styles.button + styles.fullWidth); let title_text = (first) ? script_name + ' First Time Setup' : script_name + ' Config'; message = (message) ? '&lt;p&gt;' + message + '&lt;/p&gt;' : ''; let contents = message + makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow) + '&lt;hr&gt;&lt;p style="font-size: 80%"&gt;You can always come back to this config by typing `!' + state[state_name].config.command + ' config`.&lt;/p&gt;&lt;hr&gt;' + resetButton; makeAndSendMenu(contents, title_text, 'gm'); }, makeAndSendMenu = (contents, title, whisper) =&gt; { title = (title &amp;&amp; title != '') &amp;&amp; makeTitle(title); whisper = (whisper &amp;&amp; whisper !== '') &amp;&amp; '/w ' + whisper + ' '; sendChat(script_name, whisper + '&lt;div style="' + styles.menu + styles.overflow + '"&gt;' + title + contents + '&lt;/div&gt;', null, { noarchive: true }); }, makeTitle = (title) =&gt; { return '&lt;h3 style="margin-bottom: 10px;"&gt;' + title + '&lt;/h3&gt;'; }, makeButton = (title, href, style) =&gt; { return '&lt;a style="' + style + '" href="' + href + '"&gt;' + title + '&lt;/a&gt;'; }, makeList = (items, listStyle, itemStyle) =&gt; { let list = '&lt;ul style="' + listStyle + '"&gt;'; items.forEach((item) =&gt; { list += '&lt;li style="' + itemStyle + '"&gt;' + item + '&lt;/li&gt;'; }); list += '&lt;/ul&gt;'; return list; }, pre_log = (message) =&gt; { // eslint-disable-line no-unused-vars log('---------------------------------------------------------------------------------------------'); log(message); log('---------------------------------------------------------------------------------------------'); }, checkInstall = () =&gt; { if (!_.has(state, state_name)) { state[state_name] = state[state_name] || {}; } setDefaults(); log(script_name + ' Ready! Command: !' + state[state_name].config.command); if (state[state_name].config.debug) { makeAndSendMenu(script_name + ' Ready! Debug On.', '', 'gm'); } }, observeTokenChange = function (handler) { if (handler &amp;&amp; _.isFunction(handler)) { observers.tokenChange.push(handler); } }, notifyObservers = function (event, obj, prev) { _.each(observers[event], function (handler) { handler(obj, prev); }); }, registerEventHandlers = () =&gt; { on('chat:message', handleInput); on('change:graphic', handleBarValueChange); if('undefined' !== typeof TokenMod &amp;&amp; TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(handleBarValueChange); } }, setDefaults = (reset) =&gt; { const defaults = { config: { command: 'dead', death_statusmarker: 'dead', half_statusmarker: 'red', massive_damage: true, bar: 1, firsttime: (reset) ? false : true, pc_unconscious_statusmarker: 'sleepy', change_player_tint: true, change_npc_tint: true, fx: true, fx_type: 'splatter-blood', heal_fx: false, heal_fx_type: 'glow-holy', major_injury: true, injury_statusmarker: 'broken-skull' } }; if (!state[state_name].config) { state[state_name].config = defaults.config; } else { if (!state[state_name].config.hasOwnProperty('command')) { state[state_name].config.command = defaults.config.command; } if (!state[state_name].config.hasOwnProperty('death_statusmarker')) { state[state_name].config.death_statusmarker = defaults.config.death_statusmarker; } if (!state[state_name].config.hasOwnProperty('half_statusmarker')) { state[state_name].config.half_statusmarker = defaults.config.half_statusmarker; } if (!state[state_name].config.hasOwnProperty('massive_damage')) { state[state_name].config.massive_damage = defaults.config.massive_damage; } if (!state[state_name].config.hasOwnProperty('bar')) { state[state_name].config.bar = defaults.config.bar; } if (!state[state_name].config.hasOwnProperty('pc_unconscious_statusmarker')) { state[state_name].config.pc_unconscious_statusmarker = defaults.config.pc_unconscious_statusmarker; } if (!state[state_name].config.hasOwnProperty('change_player_tint')) { state[state_name].config.change_player_tint = defaults.config.change_player_tint; } if (!state[state_name].config.hasOwnProperty('change_npc_tint')) { state[state_name].config.change_npc_tint = defaults.config.change_npc_tint; } if (!state[state_name].config.hasOwnProperty('fx')) { state[state_name].config.fx = defaults.config.fx; } if (!state[state_name].config.hasOwnProperty('fx_type')) { state[state_name].config.fx_type = defaults.config.fx_type; } if (!state[state_name].config.hasOwnProperty('heal_fx')) { state[state_name].config.heal_fx = defaults.config.heal_fx; } if (!state[state_name].config.hasOwnProperty('heal_fx_type')) { state[state_name].config.heal_fx_type = defaults.config.heal_fx_type; } if (!state[state_name].config.hasOwnProperty('major_injury')) { state[state_name].config.heal_fx_type = defaults.config.major_injury; } if (!state[state_name].config.hasOwnProperty('injury_statusmarker')) { state[state_name].config.heal_fx_type = defaults.config.injury_statusmarker; } } if (state[state_name].config.firsttime) { sendConfigMenu(true); state[state_name].config.firsttime = false; } }; on('ready', () =&gt; { checkInstall(); registerEventHandlers(); }); return { ObserveTokenChange: observeTokenChange }; })();
Awesome, after a little tweaking it now runs like a dream. My players will no longer be able to avoid the consequences of their reckless self endangerment. I thank you both, even if they most likely won't!
Callam,&nbsp; Please post the finished script.
/* globals TokenMod */ const DeathTracker = (function () { // eslint-disable-line no-unused-vars 'use strict'; let observers = { tokenChange: [] }; // Styling for the chat responses. const styles = { reset: 'padding: 0; margin: 0;', menu: 'background-color: #fff; border: 1px solid #000; padding: 5px; border-radius: 5px;', button: 'background-color: #000; border: 1px solid #292929; border-radius: 3px; padding: 5px; color: #fff; text-align: center;', list: 'list-style: none;', float: { right: 'float: right;', left: 'float: left;' }, overflow: 'overflow: hidden;', fullWidth: 'width: 100%;', underline: 'text-decoration: underline;', strikethrough: 'text-decoration: strikethrough' }, script_name = 'DeathTracker', state_name = 'DEATHTRACKER', markers = ['none', 'blue', 'brown', 'green', 'pink', 'purple', 'red', 'yellow', '-', 'all-for-one', 'angel-outfit', 'archery-target', 'arrowed', 'aura', 'back-pain', 'black-flag', 'bleeding-eye', 'bolt-shield', 'broken-heart', 'broken-shield', 'broken-skull', 'chained-heart', 'chemical-bolt', 'cobweb', 'dead', 'death-zone', 'drink-me', 'edge-crack', 'fishing-net', 'fist', 'fluffy-wing', 'flying-flag', 'frozen-orb', 'grab', 'grenade', 'half-haze', 'half-heart', 'interdiction', 'lightning-helix', 'ninja-mask', 'overdrive', 'padlock', 'pummeled', 'radioactive', 'rolling-tomb', 'screaming', 'sentry-gun', 'skull', 'sleepy', 'snail', 'spanner', 'stopwatch', 'strong', 'three-leaves', 'tread', 'trophy', 'white-tower'], handleInput = (msg) =&gt; { if (msg.type != 'api' || !playerIsGM(msg.playerid)) return; // Split the message into command and argument(s) let args = msg.content.split(' '); let command = args.shift().substring(1); let extracommand = args.shift(); if (command == state[state_name].config.command) { switch (extracommand) { case 'reset': state[state_name] = {}; setDefaults(true); sendConfigMenu(); break; case 'config': { let message; if (args.length &gt; 0) { let setting = args.shift().split('|'); let key = setting.shift(); let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; if (key === 'death_statusmarker') { if (value !== state[state_name].config.half_statusmarker) { state[state_name].config[key] = value; } else { message = '&lt;span style="color: red"&gt;Statusmakers can\'t be the same.&lt;/span&gt;'; } } else if (key === 'half_statusmarker') { if (value !== state[state_name].config.death_statusmarker) { state[state_name].config[key] = value; } else { message = '&lt;span style="color: red"&gt;Statusmakers can\'t be the same.&lt;/span&gt;'; } } else { state[state_name].config[key] = value; } if (key === 'bar') { //registerEventHandlers(); message = '&lt;span style="color: red"&gt;The API Sandbox needs to be restarted for this to take effect.&lt;/span&gt;'; } } sendConfigMenu(false, message); } break; default: sendConfigMenu(); break; } } }, handleBarValueChange = (obj, prev) =&gt; { let bar = 'bar' + state[state_name].config.bar; if (!obj || !prev || !obj.get('represents') || obj.get(bar + '_value') === prev[bar + '_value']) { return; // Don't continue if values are the same, or token is not a character. } // Get current and prev hp, and calc damage taken. let currentHP = parseInt(obj.get(bar + '_value')), prevCurrentHP = parseInt(prev[bar + '_value']), maxHP = parseInt(obj.get(bar + '_max')), damageTaken = prevCurrentHP - currentHP; let attributes = {}; // Get marker settings. let deathMarker = state[state_name].config.death_statusmarker, halfMarker = state[state_name].config.half_statusmarker, majorInjury = state[state_name].config.injury_statusmarker, unconsciousMarker = state[state_name].config.pc_unconscious_statusmarker; // Get playerid let playerid = (obj.get('controlledby') &amp;&amp; obj.get('controlledby') !== '') ? obj.get('controlledby') : (getObj('character', obj.get('represents'))) ? getObj('character', obj.get('represents')).get('controlledby') : false; // Check if it is a player. let isPlayer = (playerid &amp;&amp; !playerIsGM(playerid)); let marker; // If no death marker is set yet, and target becomes below 0 hp, add marker. if (deathMarker !== 'none' &amp;&amp; currentHP &lt;= 0) { if(!isPlayer){ marker = deathMarker; }else if(state[state_name].config.massive_damage &amp;&amp; damageTaken &gt; prevCurrentHP &amp;&amp; (damageTaken - prevCurrentHP) &gt; maxHP){ // MASSIVE DAMAGE marker = deathMarker; makeAndSendMenu(obj.get('name') + ' took massive damage, and instantly died.', '', ''); }else{ marker = (unconsciousMarker !== 'none') ? unconsciousMarker : deathMarker; } attributes['status_' + marker] = true; attributes['status_' + halfMarker] = false; // Remove halfmarker. } else { // If hp is above 0, remove markers, check for halfmarker. attributes['status_' + deathMarker] = false; attributes['status_' + unconsciousMarker] = false; attributes['status_' + halfMarker] = (halfMarker !== 'none' &amp;&amp; maxHP !== '' &amp;&amp; currentHP &lt;= maxHP / 2); } //Major Injury Notifier if(state[state_name].config.major_injury &amp;&amp; damageTaken &gt; (maxHP / 2) &amp;&amp; currentHP &lt; prevCurrentHP &amp;&amp; prevCurrentHP &gt; 0 &amp;&amp; isPlayer){ marker = majorInjury; makeAndSendMenu(obj.get('name') + ' must make a saving throw to avoid suffering a major injury.', '', ''); } // Do tints if(state[state_name].config.change_player_tint &amp;&amp; isPlayer || state[state_name].config.change_npc_tint &amp;&amp; !isPlayer){ let color = getColor(1 - (currentHP / maxHP)); attributes.tint_color = (maxHP == currentHP) ? 'transparent' : color; } // Do damage fx if(state[state_name].config.fx &amp;&amp; currentHP &lt; parseInt(prev[bar + '_value'])){ let x = parseInt(obj.get('left')), y = parseInt(obj.get('top')); spawnFxBetweenPoints({ x, y }, { x, y }, state[state_name].config.fx_type, obj.get('pageid')); } // Do heal fx if(state[state_name].config.heal_fx &amp;&amp; currentHP &gt; parseInt(prev[bar + '_value'])){ let x = parseInt(obj.get('left')), y = parseInt(obj.get('top')); spawnFxBetweenPoints({ x, y }, { x, y }, state[state_name].config.heal_fx_type, obj.get('pageid')); } obj.set(attributes); notifyObservers('tokenChange', obj, prev); }, getColor = (value) =&gt; { return hslToHex(((1-value)*120), 75, 50); }, hslToHex = (h, s, l) =&gt; { h /= 360; s /= 100; l /= 100; let r, g, b; if (s === 0) { r = g = b = l; // achromatic } else { const hue2rgb = (p, q, t) =&gt; { if (t &lt; 0) t += 1; if (t &gt; 1) t -= 1; if (t &lt; 1 / 6) return p + (q - p) * 6 * t; if (t &lt; 1 / 2) return q; if (t &lt; 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l &lt; 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } const toHex = x =&gt; { const hex = Math.round(x * 255).toString(16); return hex.length === 1 ? '0' + hex : hex; }; return `#${toHex(r)}${toHex(g)}${toHex(b)}`; }, ucFirst = (string) =&gt; { return string.charAt(0).toUpperCase() + string.slice(1); }, sendConfigMenu = (first, message) =&gt; { let markerDropdown = '?{Marker'; markers.forEach((marker) =&gt; { markerDropdown += '|' + ucFirst(marker).replace('-', ' ') + ',' + marker; }); markerDropdown += '}'; let massiveDmgButton = makeButton(state[state_name].config.massive_damage, '!' + state[state_name].config.command + ' config massive_damage|' + !state[state_name].config.massive_damage, styles.button + styles.float.right), death_markerButton = makeButton(state[state_name].config.death_statusmarker, '!' + state[state_name].config.command + ' config death_statusmarker|' + markerDropdown, styles.button + styles.float.right), half_markerButton = makeButton(state[state_name].config.half_statusmarker, '!' + state[state_name].config.command + ' config half_statusmarker|' + markerDropdown, styles.button + styles.float.right), commandButton = makeButton('!' + state[state_name].config.command, '!' + state[state_name].config.command + ' config command|?{Command (without !)}', styles.button + styles.float.right), barButton = makeButton('bar ' + state[state_name].config.bar, '!' + state[state_name].config.command + ' config bar|?{Bar|Bar 1 (green),1|Bar 2 (blue),2|Bar 3 (red),3}', styles.button + styles.float.right), pc_unconscious_markerButton = makeButton(state[state_name].config.pc_unconscious_statusmarker, '!' + state[state_name].config.command + ' config pc_unconscious_statusmarker|' + markerDropdown, styles.button + styles.float.right), change_player_tintButton = makeButton(state[state_name].config.change_player_tint, '!' + state[state_name].config.command + ' config change_player_tint|' + !state[state_name].config.change_player_tint, styles.button + styles.float.right), change_npc_tintButton = makeButton(state[state_name].config.change_npc_tint, '!' + state[state_name].config.command + ' config change_npc_tint|' + !state[state_name].config.change_npc_tint, styles.button + styles.float.right), fxButton = makeButton(state[state_name].config.fx, '!' + state[state_name].config.command + ' config fx|' + !state[state_name].config.fx, styles.button + styles.float.right), fx_typeButton = makeButton(state[state_name].config.fx_type, '!' + state[state_name].config.command + ' config fx_type|?{FX Type|'+state[state_name].config.fx_type+'}', styles.button + styles.float.right), heal_fxButton = makeButton(state[state_name].config.heal_fx, '!' + state[state_name].config.command + ' config heal_fx|' + !state[state_name].config.heal_fx, styles.button + styles.float.right), heal_fx_typeButton = makeButton(state[state_name].config.heal_fx_type, '!' + state[state_name].config.command + ' config heal_fx_type|?{Heal FX Type|'+state[state_name].config.heal_fx_type+'}', styles.button + styles.float.right), majorInjuryButton = makeButton(state[state_name].config.major_injury, '!' + state[state_name].config.command + ' config major_injury| ' + !state[state_name].config.major_injury, styles.button + styles.float.right), injury_markerButton = makeButton(state[state_name].config.injury_statusmarker, '!' + state[state_name].config.command + ' config injury_statusmarker|' + markerDropdown, styles.button + styles.float.right), listItems = [ '&lt;span style="'+styles.float.left+'"&gt;Command:&lt;/span&gt; ' + commandButton, '&lt;span style="'+styles.float.left+'"&gt;Massive Damage:&lt;/span&gt; ' + massiveDmgButton, '&lt;span style="'+styles.float.left+'"&gt;HP Bar:&lt;/span&gt; ' + barButton, '&lt;span style="'+styles.float.left+'"&gt;Dead Statusmarker:&lt;/span&gt; ' + death_markerButton, '&lt;span style="'+styles.float.left+'"&gt;Uncon. Statusmarker:&lt;div style="font-size: 8pt"&gt;Unconscious marker if PC.&lt;/div&gt;&lt;/span&gt; ' + pc_unconscious_markerButton, '&lt;span style="'+styles.float.left+'"&gt;Half Dead Statusmarker:&lt;/span&gt; ' + half_markerButton, '&lt;span style="'+styles.float.left+'"&gt;Change Player Tint Color:&lt;/span&gt; ' + change_player_tintButton, '&lt;span style="'+styles.float.left+'"&gt;Change NPC Tint Color:&lt;/span&gt; ' + change_npc_tintButton, '&lt;span style="'+styles.float.left+'"&gt;Use FX:&lt;div style="font-size: 8pt"&gt;When getting damage.&lt;/div&gt;&lt;/span&gt; ' + fxButton, '&lt;span style="'+styles.float.left+'"&gt;Major Injury:&lt;/span&gt; ' + majorInjuryButton, '&lt;span style="'+styles.float.left+'"&gt;Major Injury Statusmarker:&lt;/span&gt; ' + injury_markerButton ]; if(state[state_name].config.fx){ listItems.push('&lt;span style="'+styles.float.left+'"&gt;FX Type:&lt;/span&gt; ' + fx_typeButton); } listItems.push('&lt;span style="'+styles.float.left+'"&gt;Use Heal FX:&lt;/span&gt; ' + heal_fxButton); if(state[state_name].config.heal_fx){ listItems.push('&lt;span style="'+styles.float.left+'"&gt;Heal FX Type:&lt;/span&gt; ' + heal_fx_typeButton); } let resetButton = makeButton('Reset', '!' + state[state_name].config.command + ' reset', styles.button + styles.fullWidth); let title_text = (first) ? script_name + ' First Time Setup' : script_name + ' Config'; message = (message) ? '&lt;p&gt;' + message + '&lt;/p&gt;' : ''; let contents = message + makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow) + '&lt;hr&gt;&lt;p style="font-size: 80%"&gt;You can always come back to this config by typing `!' + state[state_name].config.command + ' config`.&lt;/p&gt;&lt;hr&gt;' + resetButton; makeAndSendMenu(contents, title_text, 'gm'); }, makeAndSendMenu = (contents, title, whisper) =&gt; { title = (title &amp;&amp; title != '') &amp;&amp; makeTitle(title); whisper = (whisper &amp;&amp; whisper !== '') &amp;&amp; '/w ' + whisper + ' '; sendChat(script_name, whisper + '&lt;div style="' + styles.menu + styles.overflow + '"&gt;' + title + contents + '&lt;/div&gt;', null, { noarchive: true }); }, makeTitle = (title) =&gt; { return '&lt;h3 style="margin-bottom: 10px;"&gt;' + title + '&lt;/h3&gt;'; }, makeButton = (title, href, style) =&gt; { return '&lt;a style="' + style + '" href="' + href + '"&gt;' + title + '&lt;/a&gt;'; }, makeList = (items, listStyle, itemStyle) =&gt; { let list = '&lt;ul style="' + listStyle + '"&gt;'; items.forEach((item) =&gt; { list += '&lt;li style="' + itemStyle + '"&gt;' + item + '&lt;/li&gt;'; }); list += '&lt;/ul&gt;'; return list; }, pre_log = (message) =&gt; { // eslint-disable-line no-unused-vars log('---------------------------------------------------------------------------------------------'); log(message); log('---------------------------------------------------------------------------------------------'); }, checkInstall = () =&gt; { if (!_.has(state, state_name)) { state[state_name] = state[state_name] || {}; } setDefaults(); log(script_name + ' Ready! Command: !' + state[state_name].config.command); if (state[state_name].config.debug) { makeAndSendMenu(script_name + ' Ready! Debug On.', '', 'gm'); } }, observeTokenChange = function (handler) { if (handler &amp;&amp; _.isFunction(handler)) { observers.tokenChange.push(handler); } }, notifyObservers = function (event, obj, prev) { _.each(observers[event], function (handler) { handler(obj, prev); }); }, registerEventHandlers = () =&gt; { on('chat:message', handleInput); on('change:graphic', handleBarValueChange); if('undefined' !== typeof TokenMod &amp;&amp; TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(handleBarValueChange); } }, setDefaults = (reset) =&gt; { const defaults = { config: { command: 'dead', death_statusmarker: 'dead', half_statusmarker: 'red', massive_damage: true, bar: 1, firsttime: (reset) ? false : true, pc_unconscious_statusmarker: 'sleepy', change_player_tint: true, change_npc_tint: true, fx: true, fx_type: 'splatter-blood', heal_fx: false, heal_fx_type: 'glow-holy', major_injury: true, injury_statusmarker: 'broken-skull' } }; if (!state[state_name].config) { state[state_name].config = defaults.config; } else { if (!state[state_name].config.hasOwnProperty('command')) { state[state_name].config.command = defaults.config.command; } if (!state[state_name].config.hasOwnProperty('death_statusmarker')) { state[state_name].config.death_statusmarker = defaults.config.death_statusmarker; } if (!state[state_name].config.hasOwnProperty('half_statusmarker')) { state[state_name].config.half_statusmarker = defaults.config.half_statusmarker; } if (!state[state_name].config.hasOwnProperty('massive_damage')) { state[state_name].config.massive_damage = defaults.config.massive_damage; } if (!state[state_name].config.hasOwnProperty('bar')) { state[state_name].config.bar = defaults.config.bar; } if (!state[state_name].config.hasOwnProperty('pc_unconscious_statusmarker')) { state[state_name].config.pc_unconscious_statusmarker = defaults.config.pc_unconscious_statusmarker; } if (!state[state_name].config.hasOwnProperty('change_player_tint')) { state[state_name].config.change_player_tint = defaults.config.change_player_tint; } if (!state[state_name].config.hasOwnProperty('change_npc_tint')) { state[state_name].config.change_npc_tint = defaults.config.change_npc_tint; } if (!state[state_name].config.hasOwnProperty('fx')) { state[state_name].config.fx = defaults.config.fx; } if (!state[state_name].config.hasOwnProperty('fx_type')) { state[state_name].config.fx_type = defaults.config.fx_type; } if (!state[state_name].config.hasOwnProperty('heal_fx')) { state[state_name].config.heal_fx = defaults.config.heal_fx; } if (!state[state_name].config.hasOwnProperty('heal_fx_type')) { state[state_name].config.heal_fx_type = defaults.config.heal_fx_type; } if (!state[state_name].config.hasOwnProperty('major_injury')) { state[state_name].config.heal_fx_type = defaults.config.major_injury; } if (!state[state_name].config.hasOwnProperty('injury_statusmarker')) { state[state_name].config.heal_fx_type = defaults.config.injury_statusmarker; } } if (state[state_name].config.firsttime) { sendConfigMenu(true); state[state_name].config.firsttime = false; } }; on('ready', () =&gt; { checkInstall(); registerEventHandlers(); }); return { ObserveTokenChange: observeTokenChange }; })();