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] CombatTracker

1526330129

Edited 1526330232
Robin
API Scripter
teak421 said: Do you want bug posts here or on Github?  I've posted a few on Github... Want to make sure I'm putting them where you check.  Thanks! teak I check both, so doesn't matter much :) Less time this week tho, but I will look into them.
This is a great script. Should the shift ping in version 2 be moving the other players maps as well? I couldn't seem to get that to work.
1526392079

Edited 1526392090
Should we only be using the scripts in the OneClick Installer? I've been looking at using the top level script and keep getting the following errors. Also, I've noticed sometimes when this is running, CTRL+U changes to Inspect Element in Chrome.
1526393540
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Are you copying your script from the "Raw" page? That will ensure you don't get any spurious text
1526405358

Edited 1526405392
The Aaron
Pro
API Scripter
The problem is that getCurrentTurn() returns the first entry of the array and handleGraphicMovement() assumes there will be a valid object there.  When the Turn Order is empty, it will return undefined.  Changing it to this:     handleGraphicMovement = (obj, prev) => {         if(!inFight()) return;         if( (getCurrentTurn()||{id:null}) .id === obj.get('id')){             changeMarker(obj);         }     }, will prevent the issue.
keithcurtis said: Are you copying your script from the "Raw" page? That will ensure you don't get any spurious text I was copying it from the GIT repository as another thread mentioned shift pinging the map. Both the top level CombatTracket.jt and the latest sub folder. Both gave me those errors.
1526432937
The Aaron
Pro
API Scripter
Robin, you might like this fixedSendPing() function from TurnMarker1:     fixedSendPing = (function(){         var last={};         return function(left,top,pageid,playerid,pull){             if( last.left   === left   &&                 last.top    === top    &&                 last.pageid === pageid ) {                 sendPing(-100,-100,pageid,null,false);             }             sendPing(left,top,pageid,playerid,pull);             last.left=left;             last.top=top;             last.pageid=pageid;         };     }()), The built-in sendPing() has an issue where if you ping pull the same location twice in a row from the API, it won't ping again.  This function detects that and sends a ping off screen before sending the requested ping again.  That situation may not come up for you, but if you decide to implement a button for pulling the GM, you'll run into it at some point and wonder WTF is going on. =D
Jeremy R. said: Should we only be using the scripts in the OneClick Installer? I've been looking at using the top level script and keep getting the following errors. Also, I've noticed sometimes when this is running, CTRL+U changes to Inspect Element in Chrome. My script's being disabled with a similar error, be it installed from the repository, be it copied from the git :( TypeError: Cannot read property 'id' of null TypeError: Cannot read property 'id' of null at turnorder.forEach.turn (apiscript.js:8125:20) at Array.forEach (native) at checkMarkerturn (apiscript.js:8124:19) at getOrCreateMarker (apiscript.js:8094:9) at resetMarker (apiscript.js:7841:22) at startCombat (apiscript.js:7791:9) at handleInput (apiscript.js:7474:17) at eval (eval at (/home/node/d20-api-server/api.js:151:1), :65:16) at Object.publish (eval at (/home/node/d20-api-server/api.js:151:1), :70:8) at /home/node/d20-api-server/api.js:1634:12
1526678271
Robin
API Scripter
I will look into all the errors and such soon, have less time last couple of days. Will have more time again soon.
Hi, so I am a few sessions into a campaign using this script along with your status script. Earlier I asked if you would be implementing a way to review the messages of a status already active on a token, and you said it would likely be added. Can you give any indication on how far out this is, if its still coming. Compared to TrackerJacker, its the one thing I keep missing. Not having to memorize the penalties applied by an effect could really be a time saver, as I currently have to look the same stuff up multiple times. Anyways, the scripts work nicely, without any hickups over a dozen engagements. Players are happy too.
This is an amazing script that is really helpful to me and all my players. I've been using TrackerJacker for forever and this is like a more intuitive and supported version of it so thank you! I was wondering if there is a way to fix a specific problem that I've had in my games though. We sometimes use a rule in our 5e games where we reroll initiative each round. I've been using the GroupInitiative script by The Aaron and it's reroll function for this, but with your script it will also roll a new initiative for the round counter/turn marker. Is there a way to get around this issue? Perhaps a way to stop it from rerolling along with everyone else, somehow giving it a static -20 to its initiative so it's guaranteed to be the last in the turn order, or maybe somehow refreshing its -1 initiative like when you first start up the combat tracker? Thanks again for your amazing work!
Also I may have found a small bug in the script. Whenever I start up combat tracker with an existing turn order, the marker doesn't immediately move to the token and the timer doesn't start until the next turn in the order. It's a small issue but I thought I should mention it!
Is there a way to get the script to automatically roll a character macro at the beginning of a token's turn? I'm using the Shaped Sheet, and I find it helpful for me and my players to have the current token Statblock displayed on the chat whenever it is that character's time to act.  As an alternative, if one could extra bind buttons on the new round message, it would also do the trick. Any ideas?
Hi Robin, I'm new to API scripts and I was looking for something that will make combat run much more smoothly in my games. This script is excellent and it meets my need to track the number of turns that have gone by and the time remaining on conditions. Great job! The only problem is that I thought by using this in conjunction with statusinfo that I could use statusinfo to add the condition marker to the tokens, but when I add a condition to a token through CombatTracker it doesn't add the marker to the token. Should StatusInfo be doing this? I have tried the command !ct conditions, but it just returns !StatusInfo not installed - even though I do have it installed through the one click. I'm sure I'm suffering from luddite blindness ( a condition peculiar to me), but I would appreciate any advice you have.
Grant H. said: Hi Robin, I'm new to API scripts and I was looking for something that will make combat run much more smoothly in my games. This script is excellent and it meets my need to track the number of turns that have gone by and the time remaining on conditions. Great job! The only problem is that I thought by using this in conjunction with statusinfo that I could use statusinfo to add the condition marker to the tokens, but when I add a condition to a token through CombatTracker it doesn't add the marker to the token. Should StatusInfo be doing this? I have tried the command !ct conditions, but it just returns !StatusInfo not installed - even though I do have it installed through the one click. I'm sure I'm suffering from luddite blindness ( a condition peculiar to me), but I would appreciate any advice you have. Hey Grant, I'm didn't write the script but I believe the problem is that the current version of StatusInfo in the one-click installer is 0.3.6. This version isn't set up to be completely compatible with CombatTracker yet. You can get the newest version of StatusInfo (0.3.8) from Robin's Github page and that one works perfectly. I'm not sure when it will make its way to the one-click though.
Hi Connor - thanks, I didn't think about checking that :)
Earlier today CombatTracker was working fine.  When I went to run my second game of the night, it wouldn't work at all, and I couldn't even send tokens to the initiative tracker manually, nor could my players click on their tokens and roll initiative directly into the tracker.  I keep getting the following error message on the API output console.  Anyone know what the problem is or how to fix it? TypeError: Cannot read property 'id' of null TypeError: Cannot read property 'id' of null at turnorder.forEach.turn (apiscript.js:798:20) at Array.forEach (native) at checkMarkerturn (apiscript.js:797:19) at getOrCreateMarker (apiscript.js:767:9) at resetMarker (apiscript.js:514:22) at startCombat (apiscript.js:464:9) at handleInput (apiscript.js:147:17) at eval (eval at <anonymous> (/home/node/d20-api-server/api.js:151:1), <anonymous>:65:16) at Object.publish (eval at <anonymous> (/home/node/d20-api-server/api.js:151:1), <anonymous>:70:8) at /home/node/d20-api-server/api.js:1634:12
1529346052
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
I was having the exact same problem the other day. I couldn't even add turn items using the Turn Tracker dialog box. Drove me crazy. I even duped my campaign to test, since it wasn't happening in any other game. Finally, I opened the Turn Tracker dialog box and told it to clear all existing turns. It occurred to me that it should have the message indicating a lack of characters in the tracker, but instead it was completely blank. After I did this, everything worked normally. FWIW, this was my first game using the Combat Tracker script. I don't know if it is at all related, but I had never had such an error before. Fortunately it was an easy fix.
1529347120
The Aaron
Pro
API Scripter
if you change line 798 to this:             if( turn && turn.id === marker.get('id')) hasTurn = true; It should prevent the crash.
1529438929

Edited 1529438957
I was wondering if there is a way to fix a specific problem that I've had in my games though. We sometimes use a rule in our 5e games where we reroll initiative each round. I've been using the GroupInitiative script by The Aaron and it's reroll function for this, but with your script it will also roll a new initiative for the round counter/turn marker. Is there a way to get around this issue? Perhaps a way to stop it from rerolling along with everyone else, somehow giving it a static -20 to its initiative so it's guaranteed to be the last in the turn order, or maybe somehow refreshing its -1 initiative like when you first start up the combat tracker? Just in case anyone was interested, I ended up finding a way to edit Robin's code a bit to allow for characters with advantage on initiative rolls and rerolling initiative during combat. It's not a pretty or elegant solution and it only works for the 5e OGL character sheet, but if you'd like to see it just send me a message!
Robin said: Anders said: Great that you have added the !ct show, but if I can ask a bit more, could it also display the messages of the status, or have a button to do so. The reason I asked for !ct show was that often I need to be reminded what effects are on an enemy - and what those effects do - often out of turn (like when they are attacked by the active character). For that reason, being able to be reminded of the full effect at any time would be nice. Hope you will be able to help me out, but in any case I love your work - and will be using it going forward. Yeh, I want to do a lot more styling and information. So this will probably be a feature later on. Dethelm said: I could not find this in a previous post: I noticed that when the turn maker moves from a token that is on the GM layer to a token on the object and token layer, the players can see where the turn maker comes from. Also, is it possible to add autoskip on Custom Items added to turnorder, so when a custom item comes after a player it skips to next player/monster and does not get "stuck"? ah yes, need to look into that. Will do tomorrow. The second thing to. Hi Robin Any chance you will implement the changes i suggested? Maybe just a short reply if you are planning on improving CombatTracker with all/some of the suggestions that have been ticking in recently or you have moved on to other stuff..
1529784327
Robin
API Scripter
Oke, I think I can make more time now. Sorry for the delay people, back to programming! Jeremy said: Should we only be using the scripts in the OneClick Installer? I've been looking at using the top level script and keep getting the following errors. Also, I've noticed sometimes when this is running, CTRL+U changes to Inspect Element in Chrome. Fixed in the next version with TheAaron's fix. The Aaron said: Robin, you might like this fixedSendPing() function from TurnMarker1:     fixedSendPing = (function(){         var last={};         return function(left,top,pageid,playerid,pull){             if( last.left   === left   &&                 last.top    === top    &&                 last.pageid === pageid ) {                 sendPing(-100,-100,pageid,null,false);             }             sendPing(left,top,pageid,playerid,pull);             last.left=left;             last.top=top;             last.pageid=pageid;         };     }()), The built-in sendPing() has an issue where if you ping pull the same location twice in a row from the API, it won't ping again.  This function detects that and sends a ping off screen before sending the requested ping again.  That situation may not come up for you, but if you decide to implement a button for pulling the GM, you'll run into it at some point and wonder WTF is going on. =D Thank you, will take a look into this. Anders said: Hi, so I am a few sessions into a campaign using this script along with your status script. Earlier I asked if you would be implementing a way to review the messages of a status already active on a token, and you said it would likely be added. Can you give any indication on how far out this is, if its still coming. Compared to TrackerJacker, its the one thing I keep missing. Not having to memorize the penalties applied by an effect could really be a time saver, as I currently have to look the same stuff up multiple times. Anyways, the scripts work nicely, without any hickups over a dozen engagements. Players are happy too. Probably in the next update. Sorry for the delay. Connor said: This is an amazing script that is really helpful to me and all my players. I've been using TrackerJacker for forever and this is like a more intuitive and supported version of it so thank you! I was wondering if there is a way to fix a specific problem that I've had in my games though. We sometimes use a rule in our 5e games where we reroll initiative each round. I've been using the GroupInitiative script by The Aaron and it's reroll function for this, but with your script it will also roll a new initiative for the round counter/turn marker. Is there a way to get around this issue? Perhaps a way to stop it from rerolling along with everyone else, somehow giving it a static -20 to its initiative so it's guaranteed to be the last in the turn order, or maybe somehow refreshing its -1 initiative like when you first start up the combat tracker? Thanks again for your amazing work! Hmm, will take a look into this. Calliban said: Is there a way to get the script to automatically roll a character macro at the beginning of a token's turn? I'm using the Shaped Sheet, and I find it helpful for me and my players to have the current token Statblock displayed on the chat whenever it is that character's time to act.  As an alternative, if one could extra bind buttons on the new round message, it would also do the trick. Any ideas? I like the idea of things happening when it's a players turn, will take a look into this. Dethelm said: Robin said: Anders said: Great that you have added the !ct show, but if I can ask a bit more, could it also display the messages of the status, or have a button to do so. The reason I asked for !ct show was that often I need to be reminded what effects are on an enemy - and what those effects do - often out of turn (like when they are attacked by the active character). For that reason, being able to be reminded of the full effect at any time would be nice. Hope you will be able to help me out, but in any case I love your work - and will be using it going forward. Yeh, I want to do a lot more styling and information. So this will probably be a feature later on. Dethelm said: I could not find this in a previous post: I noticed that when the turn maker moves from a token that is on the GM layer to a token on the object and token layer, the players can see where the turn maker comes from. Also, is it possible to add autoskip on Custom Items added to turnorder, so when a custom item comes after a player it skips to next player/monster and does not get "stuck"? ah yes, need to look into that. Will do tomorrow. The second thing to. Hi Robin Any chance you will implement the changes i suggested? Maybe just a short reply if you are planning on improving CombatTracker with all/some of the suggestions that have been ticking in recently or you have moved on to other stuff.. Definitely, update coming soon.
Really looking forward to this. My campaign entered a summer pause as of last week, but so far the initiative driven scenes have played out very well, in huge part thanks to your scripts. One thing we found is that when a character is up for taking a turn, all conditions tick down before the character has a chance to delay his initiative (a d20 option), effectively making his conditions tick twice. Would it be possible to have a one-click reversal of all condition-ticks, including those that ticked from 1 to removed? Alternatively, an option to disable automatic condition progression, and instead have a button on the characters turn header in chat. ...And thank you for a some great scripts.
Yey, Robin is back!
Ravenknight said: Yey, Robin is back! I felt the same way when I saw the post too.
1533470564

Edited 1533470621
I recently saw this and wanted to state that this looks like a very nice update to tracker jacker, I mostly run GURPS on Roll20 with a Custom Sheet and I plan on trying this out in a couple of campaigns that I run. I will let you know if I run into any issues! But very nice work Robin!
1533478105
SᵃᵛᵃǤᵉ
Sheet Author
API Scripter
The one thing I see is it rolls dice and adds to the basic_speed. We done use that...GURPS goes off Basis_Speed for turn order in combat. Is there any way to set dice to zero. Also looking for a way to add a modifier to that fixed number?
1533561924

Edited 1533567277
Hi there. I thought I would give this a shot again. 3 turns in and I got the following. Additionally, when I had 3 of the same NPC and added a condition to one, it was stating that all of them had the condition when their turn came up. I like the idea of this, as the Combat Tracker is the one element Fantasy Grounds has that I really like. However, I just keep running into errors with this. Edit: I have not been able to re-produce the error message below, which is good news. Still, is there any way to eliminate the issue where the same condition is being applied to each instance of the same creature?
The orange combat tracker seems to have some strange behavior, I am guessing it has something to do with the update to waypoints. It seems each turn that happens the orange tracker zips around the map quite a bit before settling in on the token. Just wanted to give a heads up. :)
I am experiencing some problems when using tokens with the same name. The solution is easy however, just give your tokens different names, perhaps via the TokenNameNumber script.
Robin, Great Script. used The Aaron;s turnmarker a lot but this combined with your statusinfo is a nice touch. Also the turn timer is awesome!! Not sure anyone has suggested this but two thing that might be nice to add. I've noticed the auto ping pull only works for the gm. The players do not get auto pulled with the turn change. This could be helpful for some if someone is zoomed on a map, and also helps a player pay attention to whats going on. One thing I also notice was the FX useage, the FX will display to players even if a token is hidden, making the fx a flashy touch but not very practical. Not sure its possible through the API but maybe adding a disable fx when a token is on the gmlayer might be worth a look.  Thats my 2 cents worth, Thanks for the all the hard work you put in into it !!
1535962579
The Aaron
Pro
API Scripter
On the ping pulling, that’s actuslly a limitation of the API. It will only pull the GM. 
I've got the same error as Jeremy a few posts above me. They pop up regularly and I am not sure of the cause. I would really like to use this script it is awesome but for me it just throws an error after a few round of combat.
1536495896
The Aaron
Pro
API Scripter
Sazra, do you have any custom turn items in your turn order?  Things that are just text and not tied to a token?
Nah, all of them are tied to tokens, I have found two possible reasons, both seemed to throw the error: 1. Using a few tokens tied to the same npc without renaming them (will try to test if numbering them helps) 2. Using the newest StatusInfo(I had a lot of cases where giving a token a status effect crashed the scripts but they always named the toFront bit of combattracker as the error cause)
1536618336
Victor B.
Pro
Sheet Author
API Scripter
Neither of those should be an issue.  I run CT with up to 30 tokens all tied to same NPC, no issues.  I also tie multiple conditions to tokens, no crashes.  I've also never seen the same condition applied to multiple tokens unless I have multiple tokens selected.  
1536618619
Victor B.
Pro
Sheet Author
API Scripter
Sazra are you running GroupInit or TokenAction or anything else like that?  CT has it's own init roller and I wonder if there's a conflict going on between CT and other scripts.   I run CT and StatusInfo and TokenMod and that is it.  
1536681324

Edited 1537001324
No, I only have CT, StatusInfo and the 5E OGL Script installed. But I will test if deactivating the last one does the trick Edit: I have tested CT now without any other scripts installed and I had the scripts crash again. The same error as before. Also I had only unique tokens and I crashed as one of my players was clicking on the "Done" button, if that is of interest.
1537211883
Victor B.
Pro
Sheet Author
API Scripter
Ok @Sazra, I'm not supporting this script but Robin's last post was 3 months ago and no updates since. I also reached out to him on Discord and no response yet.  If you want to find out what's going wrong, then please invite me to game and promote me as GM and I can take a look at what's going on.  
1537294378

Edited 1537294394
The Aaron
Pro
API Scripter
I looked at some of the issues mentioned above and made some changes in this version of the script, let me know if it resolves them for you and I'll see about pushing it into the official repo: /* * Version 0.2.1 * Made By Robin Kuiper * Changes in Version 0.2.1 by The Aaron * Skype: RobinKuiper.eu * Discord: Atheos#1095 * Roll20: <a href="https://app.roll20.net/users/1226016/robin" rel="nofollow">https://app.roll20.net/users/1226016/robin</a> * Roll20 Thread: <a href="https://app.roll20.net/forum/post/6349145/script-combattracker" rel="nofollow">https://app.roll20.net/forum/post/6349145/script-combattracker</a> * Github: <a href="https://github.com/RobinKuiper/Roll20APIScripts" rel="nofollow">https://github.com/RobinKuiper/Roll20APIScripts</a> * Reddit: <a href="https://www.reddit.com/user/robinkuiper/" rel="nofollow">https://www.reddit.com/user/robinkuiper/</a> * Patreon: <a href="https://patreon.com/robinkuiper" rel="nofollow">https://patreon.com/robinkuiper</a> * Paypal.me: <a href="https://www.paypal.me/robinkuiper" rel="nofollow">https://www.paypal.me/robinkuiper</a> */ /* TODO * * Styling * More chat message options * Show menu with B shows always * Add icon if not StatusInfo (?) (IF YES, remove conditions on statusmarker remove) * Edit Conditions */ /* globals StatusInfo, TokenMod */ var CombatTracker = CombatTracker || (function() { 'use strict'; let round = 1, timerObj, intervalHandle, rotationInterval, paused = false, observers = { tokenChange: [] }, extensions = { StatusInfo: false }; // 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;', textButton: 'background-color: transparent; border: none; padding: 0; color: #000; text-decoration: underline', 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 = 'CombatTracker', state_name = 'COMBATTRACKER', handleInput = (msg) =&gt; { if (msg.type != 'api') return; let args = msg.content.split(' '); let command = args.shift().substring(1); let extracommand = args.shift(); if(command !== state[state_name].config.command) return; if(extracommand === 'next'){ if(playerIsGM(msg.playerid) || msg.playerid === 'api'){ NextTurn(); return; } let turn = getCurrentTurn(), token = getObj('graphic', turn.id); if(token){ let character = getObj('character', token.get('represents')); if((token.get('controlledby').split(',').includes(msg.playerid) || token.get('controlledby').split(',').includes('all')) || (character &amp;&amp; (character.get('controlledby').split(',').includes(msg.playerid) || character.get('controlledby').split(',').includes(msg.playerid)))){ NextTurn(); // SHOW MENU } } } // Below commands are only for GM's if(!playerIsGM(msg.playerid)) return; let name, duration, direction, message, condition; switch(extracommand){ case 'help': sendHelpMenu(); break; case 'reset': switch(args.shift()){ case 'conditions': state[state_name].conditions = {}; break; default: state[state_name] = {}; setDefaults(true); sendConfigMenu(); break; } break; case 'config': if(args[0] === 'timer'){ if(args[1]){ let setting = args[1].split('|'); let key = setting.shift(); let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; state[state_name].config.timer[key] = value; } sendConfigTimerMenu(); }else if (args[0] === 'announcements'){ if(args[1]){ let setting = args[1].split('|'); let key = setting.shift(); let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; state[state_name].config.announcements[key] = value; } sendConfigAnnounceMenu(); }else{ if(args[0]){ let setting = args.shift().split('|'); let key = setting.shift(); let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; state[state_name].config[key] = value; } sendConfigMenu(); } break; case 'prev': PrevTurn(); break; case 'start': startCombat(msg.selected); if(args.shift() === 'b') sendMenu(); break; case 'stop': stopCombat(); if(args.shift() === 'b') sendMenu(); break; case 'st': stopTimer(); if(args.shift() === 'b') sendMenu(); break; case 'pt': pauseTimer(); if(args.shift() === 'b') sendMenu(); break; case 'conditions': sendConditionsMenu(); break; case 'show': { if(!msg.selected || !msg.selected.length){ makeAndSendMenu('No tokens are selected.', '', 'gm'); return; } let tokens = msg.selected.map(s =&gt; getObj('graphic', s._id)); sendTokenConditionMenu(tokens); } break; case 'add': name = args.shift(); if(!name){ makeAndSendMenu('No condition name was given.', '', 'gm'); return; } duration = args.shift(); duration = (!duration || duration === 0) ? 'none' : duration; direction = args.shift() || -1; message = args.join(' '); condition = { name, duration, direction, message }; if(!msg.selected || !msg.selected.length){ let tokenid = args.shift(); let token = getObj('graphic', tokenid); if(!tokenid || !token){ makeAndSendMenu('No tokens were selected.', '', 'gm'); return; } addCondition(token, condition, true); return; } msg.selected.forEach(s =&gt; { let token = getObj(s._type, s._id); if(!token) return; addCondition(token, condition, true); }); break; case 'addfav': name= args.shift(); duration = args.shift(); direction = args.shift() || -1; message = args.join(' '); condition = { name, duration, direction, message }; addOrEditFavoriteCondition(condition); sendFavoritesMenu(); break; case 'editfav': name = args.shift(); if(!name){ makeAndSendMenu('No condition name was given.', '', 'gm'); return; } name = strip(name).toLowerCase(); condition = state[state_name].favorites[name]; if(!condition){ makeAndSendMenu('Condition does not exists.', '', 'gm'); return; } if(args[0]){ let setting = args.shift().split('|'); let key = setting.shift(); let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; state[state_name].favorites[name][key] = value; if(key === 'name'){ state[state_name].favorites[strip(value).toLowerCase()] = state[state_name].favorites[name]; delete state[state_name].favorites[name]; } } sendEditFavoriteConditionMenu(condition); break; case 'removefav': removeFavoriteCondition(args.shift()); sendFavoritesMenu(); break; case 'favorites': sendFavoritesMenu(); break; case 'remove': { let cname = args.shift(); let tokenid = args.shift(); let token; if(!cname){ makeAndSendMenu('No condition was given.', '', 'gm'); return; } if(tokenid){ token = getObj('graphic', tokenid); if(token){ removeCondition(token, cname); sendTokenConditionMenu([token]); return; } } if(!msg.selected || !msg.selected.length){ makeAndSendMenu('No tokens were selected.', '', 'gm'); return; } msg.selected.forEach(s =&gt; { token = getObj(s._type, s._id); if(!token) return; removeCondition(token, cname); }); } break; default: sendMenu(); break; } }, addOrEditFavoriteCondition = (condition) =&gt; { if(condition.duration === 0 || condition.duration === '') condition.duration = undefined; let strippedName = strip(condition.name).toLowerCase(); state[state_name].favorites[strippedName] = condition; }, removeFavoriteCondition = (name) =&gt; { name = strip(name).toLowerCase(); delete state[state_name].favorites[name]; }, addCondition = (token, condition, announce=false) =&gt; { if(extensions.StatusInfo){ const duration = condition.duration; const direction = condition.direction; const message = condition.message; condition = StatusInfo.getConditionByName(condition.name) || condition; condition.duration = duration; condition.direction = direction; condition.message = message; } if(!condition.duration || condition.duration === 0 || condition.duration === '0' || condition.duration === '' || condition.duration === 'none') condition.duration = undefined; if(state[state_name].conditions[strip(token.get('name')).toLowerCase()]){ let hasCondition = false; state[state_name].conditions[strip(token.get('name')).toLowerCase()].forEach(c =&gt; { if(c.name.toLowerCase() === condition.name.toLowerCase()) hasCondition = true; }); if(hasCondition) return; state[state_name].conditions[strip(token.get('name')).toLowerCase()].push(condition); }else{ state[state_name].conditions[strip(token.get('name')).toLowerCase()] = [condition]; } if(condition.icon){ // let prevSM = token.get('statusmarkers'); token.set('status_'+condition.icon, true); if(announce &amp;&amp; extensions.StatusInfo){ StatusInfo.sendConditionToChat(condition); } }else makeAndSendMenu('Condition ' + condition.name + ' added to ' + token.get('name')); }, removeCondition = (token, condition_name, auto=false) =&gt; { if(!state[state_name].conditions[strip(token.get('name')).toLowerCase()]) return; let si_condition = false; if(extensions.StatusInfo){ si_condition = StatusInfo.getConditionByName(condition_name) || false; } state[state_name].conditions[strip(token.get('name')).toLowerCase()].forEach((condition, i) =&gt; { if(condition.name.toLowerCase() !== condition_name.toLowerCase()) return; state[state_name].conditions[strip(token.get('name')).toLowerCase()].splice(i, 1); if(si_condition){ token.set('status_'+condition.icon, false); }else if(!auto){ makeAndSendMenu('Condition ' + condition.name + ' removed from ' + token.get('name')); } }); }, strip = (str) =&gt; { return str.replace(/[^a-zA-Z0-9]+/g, '_'); }, handleTurnorderChange = (obj, prev) =&gt; { if(obj.get('turnorder') === prev.turnorder) return; let turnorder = (obj.get('turnorder') === "") ? [] : JSON.parse(obj.get('turnorder')); let prevTurnorder = (prev.turnorder === "") ? [] : JSON.parse(prev.turnorder); if(obj.get('turnorder') === "[]"){ resetMarker(); stopTimer(); return; } if(turnorder.length &amp;&amp; prevTurnorder.length &amp;&amp; turnorder[0].id !== prevTurnorder[0].id){ doTurnorderChange(); } }, handleStatusMarkerChange = (obj, prev) =&gt; { if(extensions.StatusInfo){ prev.statusmarkers = (typeof prev.get === 'function') ? prev.get('statusmarkers') : prev.statusmarkers; if(obj.get('statusmarkers') !== prev.statusmarkers){ let nS = obj.get('statusmarkers').split(','), oS = prev.statusmarkers.split(','); // Marker added? array_diff(oS, nS).forEach(icon =&gt; { if(icon === '') return; getObjects(StatusInfo.getConditions(), 'icon', icon).forEach(condition =&gt; { addCondition(obj, { name: condition.name }); }); }); // Marker Removed? array_diff(nS, oS).forEach(icon =&gt; { if(icon === '') return; getObjects(StatusInfo.getConditions(), 'icon', icon).forEach(condition =&gt; { removeCondition(obj, condition.name); }); }); } } }, handleGraphicMovement = (obj /*, prev */) =&gt; { if(!inFight()) return; if(getCurrentTurn().id === obj.get('id')){ changeMarker(obj); } }, array_diff = (a, b) =&gt; { return b.filter(function(i) {return a.indexOf(i) &lt; 0;}); }, //return an array of objects according to key, value, or key and value matching getObjects = (obj, key, val) =&gt; { var objects = []; for (var i in obj) { if (!obj.hasOwnProperty(i)) continue; if (typeof obj[i] == 'object') { objects = objects.concat(getObjects(obj[i], key, val)); } else //if key matches and value matches or if key matches and value is not passed (eliminating the case where key matches but passed value does not) if (i == key &amp;&amp; obj[i] == val || i == key &amp;&amp; val == '') { // objects.push(obj); } else if (obj[i] == val &amp;&amp; key == ''){ //only add if the object is not already in the array if (objects.lastIndexOf(obj) == -1){ objects.push(obj); } } } return objects; }, startCombat = (selected) =&gt; { paused = false; resetMarker(); Campaign().set('initiativepage', Campaign().get('playerpageid')); if(selected &amp;&amp; state[state_name].config.throw_initiative){ rollInitiative(selected, true); } }, inFight = () =&gt; { return (Campaign().get('initiativepage') !== false); }, rollInitiative = (selected, sort) =&gt; { selected.forEach(s =&gt; { if(s._type !== 'graphic') return; let token = getObj('graphic', s._id), //whisper = (token.get('layer') === 'gmlayer') ? '/w gm ' : '', bonus = parseFloat(getAttrByName(token.get('represents'), state[state_name].config.initiative_attribute_name, 'current')) || 0; let pr = randomBetween(1,20)+bonus; pr = (Math.round(pr) !== pr) ? pr.toFixed(2) : pr; addToTurnorder({ id: token.get('id'), pr, custom: '', pageid: token.get('pageid') }); }); if(sort){ sortTurnorder(); } }, stopCombat = () =&gt; { if(timerObj) timerObj.remove(); removeMarker(); stopTimer(); paused = false; Campaign().set({ initiativepage: false, turnorder: '' }); state[state_name].turnorder = {}; round = 1; }, removeMarker = () =&gt; { stopRotate(); getOrCreateMarker().remove(); }, resetMarker = () =&gt; { let marker = getOrCreateMarker(); marker.set({ name: 'Round ' + round, imgsrc: state[state_name].config.marker_img, pageid: Campaign().get('playerpageid'), layer: 'gmlayer', left: 35, top: 35, width: 70, height: 70 }); }, doTurnorderChange = (prev=false) =&gt; { if(!Campaign().get('initiativepage')) return; let turn = getCurrentTurn(); if(turn.id === '-1') return; if(turn.id === getOrCreateMarker().get('id')){ if(prev) PrevRound(); else NextRound(); return; } let token = getObj('graphic', turn.id); if(!token){ return; } toFront(token); if(state[state_name].config.timer.use_timer){ startTimer(token); } changeMarker(token || false); if(state[state_name].config.announcements.announce_turn){ announceTurn(token || turn.custom, (token.get('layer') === 'objects') ? '' : 'gm'); }else if(state[state_name].config.announcements.announce_conditions){ let name = token.get('name') || turn.custom; let conditions = getConditionString(token); if(conditions &amp;&amp; conditions !== '') makeAndSendMenu(conditions, 'Conditions - ' + name, (token.get('layer') === 'objects') ? '' : 'gm'); } Pull(token); doFX(token); }, doFX = (token) =&gt; { if(!state[state_name].config.announcements.use_fx) return; let pos = {x: token.get('left'), y: token.get('top')}; spawnFxBetweenPoints(pos, pos, state[state_name].config.announcements.fx_type, token.get('pageid')); }, Pull = (token) =&gt; { if(!state[state_name].config.pull) return; sendPing(token.get('left'), token.get('top'), token.get('pageid'), null, true); }, startTimer = (token) =&gt; { paused = false; clearInterval(intervalHandle); if(timerObj) timerObj.remove(); let config_time = parseInt(state[state_name].config.timer.time); let time = config_time; if(token &amp;&amp; state[state_name].config.timer.token_timer){ timerObj = createObj('text', { text: 'Timer: ' + time, font_size: state[state_name].config.timer.token_font_size, font_family: state[state_name].config.timer.token_font, color: state[state_name].config.timer.token_font_color, pageid: token.get('pageid'), layer: 'gmlayer' }); } intervalHandle = setInterval(() =&gt; { if(paused) return; if(timerObj) timerObj.set({ top: token.get('top')+token.get('width')/2+40, left: token.get('left'), text: 'Timer: ' + time, layer: token.get('layer') }); if(state[state_name].config.timer.chat_timer &amp;&amp; (time === config_time || config_time/2 === time || config_time/4 === time || time === 10 || time === 5)){ makeAndSendMenu('', 'Time Remaining: ' + time); } if(time &lt;= 0){ if(timerObj) timerObj.remove(); clearInterval(intervalHandle); NextTurn(); } time--; }, 1000); }, stopTimer = () =&gt; { clearInterval(intervalHandle); if(timerObj) timerObj.remove(); }, pauseTimer = () =&gt; { paused = !paused; }, announceTurn = (token, target) =&gt; { let name, imgurl; if(typeof token === 'object'){ name = token.get('name'); imgurl = token.get('imgsrc'); }else{ name = token; } let conditions = getConditionString(token); let image = (imgurl) ? '&lt;img src="'+imgurl+'" width="50px" height="50px" /&gt;' : ''; name = (state[state_name].config.announcements.handleLongName) ? handleLongString(name) : name; let contents = '\ &lt;table&gt; \ &lt;tr&gt; \ &lt;td&gt;'+image+'&lt;/td&gt; \ &lt;td style="padding-left: 5px;"&gt;&lt;span style="font-size: 16pt;"&gt;'+name+'\'s Turn&lt;/span&gt;&lt;/td&gt; \ &lt;/tr&gt; \ &lt;/table&gt; \ &lt;div style="overflow: hidden"&gt; \ &lt;div style="float: left"&gt;'+conditions+'&lt;/div&gt; \ ' + makeButton('Done', '!'+state[state_name].config.command+' next', styles.button + styles.float.right) +' \ &lt;/div&gt;'; makeAndSendMenu(contents, '', target); }, getConditionString = (token) =&gt; { let name = strip(token.get('name')).toLowerCase(); let conditionsSTR = ''; if(state[state_name].conditions[name] &amp;&amp; state[state_name].conditions[name].length){ state[state_name].conditions[name].forEach((condition, i) =&gt; { if(typeof condition.duration === 'undefined' || condition.duration === false){ conditionsSTR += '&lt;b&gt;'+condition.name+'&lt;/b&gt;&lt;br&gt;'; }else if(condition.duration &lt;= 1){ conditionsSTR += '&lt;b&gt;'+condition.name+'&lt;/b&gt; removed.&lt;br&gt;'; removeCondition(token, condition.name, true); }else{ state[state_name].conditions[name][i].duration = parseInt(state[state_name].conditions[name][i].duration)+parseInt(condition.direction); conditionsSTR += '&lt;b&gt;'+condition.name+'&lt;/b&gt;: ' + condition.duration + '&lt;br&gt;'; } conditionsSTR += (condition.message) ? '&lt;i style="font-size: 10pt"&gt;'+condition.message+'&lt;/i&gt;&lt;br&gt;' : ''; }); } return conditionsSTR; }, handleLongString = (str, max=8) =&gt; { str = str.split(' ')[0]; return (str.length &gt; max) ? str.slice(0, max) + '...' : str; }, NextTurn = () =&gt; { let turnorder = getTurnorder(), current_turn = turnorder.shift(); turnorder.push(current_turn); setTurnorder(turnorder); doTurnorderChange(); }, PrevTurn = () =&gt; { let turnorder = getTurnorder(), last_turn = turnorder.pop(); turnorder.unshift(last_turn); setTurnorder(turnorder); doTurnorderChange(true); }, NextRound = () =&gt; { let marker = getOrCreateMarker(); round++; marker.set({ name: 'Round ' + round}); if(state[state_name].config.announcements.announce_round){ let text = '&lt;span style="font-size: 16pt; font-weight: bold;"&gt;'+marker.get('name')+'&lt;/span&gt;'; makeAndSendMenu(text); } NextTurn(); }, PrevRound = () =&gt; { let marker = getOrCreateMarker(); round--; marker.set({ name: 'Round ' + round}); if(state[state_name].config.announcements.announce_round){ let text = '&lt;span style="font-size: 16pt; font-weight: bold;"&gt;'+marker.get('name')+'&lt;/span&gt;'; makeAndSendMenu(text); } PrevTurn(); }, changeMarker = (token) =&gt; { let marker = getOrCreateMarker(); if(!token){ resetMarker(); return; } let settings = { layer: token.get('layer'), top: token.get('top'), left: token.get('left'), width: token.get('width')+(token.get('width')*0.35), height: token.get('height')+(token.get('height')*0.35) }; marker.set(settings); toBack(marker); }, getOrCreateMarker = () =&gt; { let marker, img = state[state_name].config.marker_img, playerpageid = Campaign().get('playerpageid'), markers = findObjs({ pageid: playerpageid, imgsrc: img }); markers.forEach((marker, i) =&gt; { if(i &gt; 0) marker.remove(); }); marker = markers.shift(); if(!marker) { marker = createObj('graphic', { name: 'Round 0', imgsrc: img, pageid: playerpageid, layer: 'gmlayer', showplayers_name: true, left: 35, top: 35, width: 70, height: 70 }); } checkMarkerturn(marker); toBack(marker); //startRotate(marker); return marker; }, /* startRotate = (token) =&gt; { clearInterval(rotationInterval); let i = 0; rotationInterval = setInterval(() =&gt; { i += 2; log(i); if(i &gt;= 360) i = 0; token.set('rotation', i); }, 50); }, */ stopRotate = () =&gt; { clearInterval(rotationInterval); }, checkMarkerturn = (marker) =&gt; { let turnorder = getTurnorder(), hasTurn = false; turnorder.forEach(turn =&gt; { if(turn.id === marker.get('id')) hasTurn = true; }); if(!hasTurn){ turnorder.push({ id: marker.get('id'), pr: -1, custom: '', pageid: marker.get('pageid') }); Campaign().set('turnorder', JSON.stringify(turnorder)); } }, sortTurnorder = (order='DESC') =&gt; { let turnorder = getTurnorder(); turnorder.sort((a,b) =&gt; { return (order === 'ASC') ? a.pr - b.pr : b.pr - a.pr; }); setTurnorder(turnorder); doTurnorderChange(); }, getTurnorder = () =&gt; { return (Campaign().get('turnorder') === '') ? [] : Array.from(JSON.parse(Campaign().get('turnorder'))); }, getCurrentTurn = () =&gt; { return getTurnorder().shift(); }, addToTurnorder = (turn) =&gt; { if(!turn){ return; } let turnorder = getTurnorder(), justDoIt = true; turnorder.forEach(t =&gt; { if(t.id === turn.id) justDoIt = false; }); if(justDoIt){ turnorder.push(turn); setTurnorder(turnorder); } }, setTurnorder = (turnorder) =&gt; { Campaign().set('turnorder', JSON.stringify(turnorder)); }, randomBetween = (min, max) =&gt; { return Math.floor(Math.random()*(max-min+1)+min); }, sendTokenConditionMenu = (tokens) =&gt; { let contents = '&lt;table style="width: 100%;"&gt;'; let i = 0; tokens.forEach(token =&gt; { if(!token) return; let conditions = state[state_name].conditions[strip(token.get('name')).toLowerCase()]; if(i) contents += '&lt;tr&gt;&lt;td colspan="2"&gt;&lt;hr&gt;&lt;/td&gt;&lt;/tr&gt;'; i++; contents += ' \ &lt;tr&gt; \ &lt;td colspan="2" style="font-size: 12pt; font-weight: bold;"&gt; \ &lt;img src='+token.get('imgsrc')+' style="width: 32px; height: 32px; vertical-align: middle;" /&gt; \ &lt;span style="vertical-align: middle;"&gt;'+token.get('name')+'&lt;/span&gt; \ &lt;/td&gt; \ &lt;/tr&gt;'; if(!conditions || !conditions.length){ contents += '&lt;tr&gt;&lt;td colspan="2" style="text-align: center;"&gt;&lt;i&gt;None&lt;/i&gt;&lt;/td&gt;&lt;/tr&gt;'; }else{ conditions.forEach(condition =&gt; { let removeButton = makeButton('&lt;img src="<a href="https://s3.amazonaws.com/files.d20.io/images/11381509/YcG-o2Q1-CrwKD_nXh5yAA/thumb.png?1439051579" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/11381509/YcG-o2Q1-CrwKD_nXh5yAA/thumb.png?1439051579</a>" /&gt;', '!'+state[state_name].config.command + ' remove ' + condition.name + ' ' + token.get('id'), styles.button + styles.float.right + 'width: 16px; height: 16px;'); contents += ' \ &lt;tr&gt; \ &lt;td style="text-align: center"&gt;'+condition.name+'&lt;/td&gt; \ &lt;td&gt;'+removeButton+'&lt;/td&gt; \ &lt;/tr&gt;'; }); } }); contents += '&lt;/table&gt;'; makeAndSendMenu(contents, '', 'gm'); }, sendConditionsMenu = () =&gt; { let addButton; let SI_listItems = []; if(extensions.StatusInfo){ Object.keys(StatusInfo.getConditions()).map(key =&gt; StatusInfo.getConditions()[key]).forEach(condition =&gt; { let conditionSTR = condition.name + ' ?{Duration} ?{Direction|-1} ?{Message}'; addButton = makeButton(StatusInfo.getIcon(condition.icon, 'margin-right: 5px; margin-top: 5px; display: inline-block;') + condition.name, '!'+state[state_name].config.command + ' add ' + conditionSTR, styles.textButton); SI_listItems.push('&lt;span style="'+styles.float.left+'"&gt;'+addButton+'&lt;/span&gt;'); }); } let F_listItems = []; Object.keys(state[state_name].favorites).map(key =&gt; state[state_name].favorites[key]).forEach(condition =&gt; { let conditionSTR = (!condition.duration) ? condition.name : condition.name + ' ' + condition.duration + ' ' + condition.direction + ' ' + condition.message; addButton = makeButton(condition.name, '!'+state[state_name].config.command + ' add ' + conditionSTR, styles.textButton); F_listItems.push('&lt;span style="'+styles.float.left+'"&gt;'+addButton+' - &lt;span style="font-size: 8pt"&gt;'+condition.duration+':'+condition.direction+':'+condition.message+'&lt;/span&gt;&lt;/span&gt;'); }); let contents = ''; contents += '&lt;h4&gt;StatusInfo Conditions&lt;/h4&gt;'; if(SI_listItems.length){ contents += makeList(SI_listItems, styles.reset + styles.list + styles.overflow, styles.overflow); }else{ contents += (extensions.StatusInfo) ? 'Your StatusInfo doesn\'t have any conditions.' : makeButton('StatusInfo', '<a href="https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo" rel="nofollow">https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo</a>', styles.textButton) + ' is not installed.'; } contents += '&lt;hr&gt;'; contents += '&lt;h4&gt;Favorite Conditions&lt;/h4&gt;'; if(F_listItems.length){ contents += makeList(F_listItems, styles.reset + styles.list + styles.overflow, styles.overflow); }else{ contents += 'You don\'t have any favorite conditions yet.'; } contents += '&lt;br&gt;&lt;br&gt;' + makeButton('Edit Favorites', '!'+state[state_name].config.command + ' favorites', styles.button + styles.fullWidth); makeAndSendMenu(contents, 'Conditions', 'gm'); }, sendFavoritesMenu = () =&gt; { let addButton, editButton, list; let listItems = []; Object.keys(state[state_name].favorites).map(key =&gt; state[state_name].favorites[key]).forEach(condition =&gt; { let conditionSTR = (!condition.duration) ? condition.name : condition.name + ' ' + condition.duration + ' ' + condition.direction + ' ' + condition.message; addButton = makeButton(condition.name, '!'+state[state_name].config.command + ' add ' + conditionSTR, styles.textButton); editButton = makeButton('Edit', '!'+state[state_name].config.command + ' editfav ' + condition.name, styles.button + styles.float.right); listItems.push('&lt;span style="'+styles.float.left+'"&gt;'+addButton+'&lt;/span&gt; '+editButton); }); let newButton = makeButton('Add New', '!'+state[state_name].config.command + ' addfav ?{Name} ?{Duration} ?{Direction} ?{Message}', styles.button + styles.fullWidth); list = (listItems.length) ? makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow) : 'No favorites yet.'; makeAndSendMenu(list + '&lt;hr&gt;' + newButton, 'Favorite Conditions', 'gm'); }, sendEditFavoriteConditionMenu = (condition) =&gt; { if(!state[state_name].favorites[strip(condition.name).toLowerCase()]){ makeAndSendMenu('Condition does not exist.', '', 'gm'); return; } let nameButton = makeButton(condition.name, '!'+state[state_name].config.command + ' editfav ' + condition.name + ' name|?{Name|'+condition.name+'}', styles.button + styles.float.right); let durationButton = makeButton(condition.duration, '!'+state[state_name].config.command + ' editfav ' + condition.name + ' duration|?{Duration|'+condition.duration+'}', styles.button + styles.float.right); let directionButton = makeButton(condition.direction, '!'+state[state_name].config.command + ' editfav ' + condition.name + ' direction|?{Direction|'+condition.direction+'}', styles.button + styles.float.right); let listItems = [ '&lt;span style="'+styles.float.left+'"&gt;Name&lt;/span&gt; '+nameButton, '&lt;span style="'+styles.float.left+'"&gt;Duration&lt;/span&gt; '+durationButton ]; if(condition.duration &amp;&amp; condition.duration !== 0 &amp;&amp; condition.duration !== '0'){ listItems.push('&lt;span style="'+styles.float.left+'"&gt;Direction&lt;/span&gt; '+directionButton); } let removeButton = makeButton('Remove', '!'+state[state_name].config.command + ' removefav ' + condition.name, styles.button + styles.fullWidth); let backButton = makeButton('Back', '!'+state[state_name].config.command + ' favorites', styles.button + styles.fullWidth); let messageButton = makeButton((condition.message) ? 'Change Message' : 'Set Message', '!'+state[state_name].config.command + ' editfav ' + condition.name + ' message|?{Message|'+condition.message+'}', styles.button); let message = (condition.message) ? condition.message : '&lt;i&gt;None&lt;/i&gt;'; makeAndSendMenu(makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow) + '&lt;hr&gt;&lt;b&gt;Message&lt;/b&gt;&lt;p&gt;' + message + '&lt;/p&gt;' + messageButton + '&lt;hr&gt;' + removeButton + '&lt;hr&gt;' + backButton, 'Edit - ' + condition.name, 'gm'); }, sendConfigMenu = (first, message) =&gt; { let commandButton = makeButton('!'+state[state_name].config.command, '!' + state[state_name].config.command + ' config command|?{Command (without !)}', styles.button + styles.float.right), markerImgButton = makeButton('&lt;img src="'+state[state_name].config.marker_img+'" width="30px" height="30px" /&gt;', '!' + state[state_name].config.command + ' config marker_img|?{Image Url}', styles.button + styles.float.right), throwIniButton = makeButton(state[state_name].config.throw_initiative, '!' + state[state_name].config.command + ' config throw_initiative|'+!state[state_name].config.throw_initiative, styles.button + styles.float.right), iniAttrButton = makeButton(state[state_name].config.initiative_attribute_name, '!' + state[state_name].config.command + ' config initiative_attribute_name|?{Attribute|'+state[state_name].config.initiative_attribute_name+'}', styles.button + styles.float.right), closeStopButton = makeButton(state[state_name].config.close_stop, '!' + state[state_name].config.command + ' config close_stop|'+!state[state_name].config.close_stop, styles.button + styles.float.right), pullButton = makeButton(state[state_name].config.pull, '!' + state[state_name].config.command + ' config pull|'+!state[state_name].config.pull, 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;Ini. Attribute:&lt;/span&gt; ' + iniAttrButton, '&lt;span style="'+styles.float.left+'"&gt;Marker Img:&lt;/span&gt; ' + markerImgButton, '&lt;span style="'+styles.float.left+'"&gt;Stop on close:&lt;/span&gt; ' + closeStopButton, '&lt;span style="'+styles.float.left+'"&gt;Auto Roll Ini.:&lt;/span&gt; ' + throwIniButton, '&lt;span style="'+styles.float.left+'"&gt;Auto Pull Map:&lt;/span&gt; ' + pullButton ], configTimerButton = makeButton('Timer Config', '!'+state[state_name].config.command + ' config timer', styles.button), configAnnouncementsButton = makeButton('Announcement Config', '!'+state[state_name].config.command + ' config announcements', styles.button), resetButton = makeButton('Reset', '!' + state[state_name].config.command + ' reset', styles.button + styles.fullWidth), 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)+configTimerButton+configAnnouncementsButton+'&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'); }, sendConfigAnnounceMenu = () =&gt;{ let announceTurnButton = makeButton(state[state_name].config.announcements.announce_turn, '!' + state[state_name].config.command + ' config announcements announce_turn|'+!state[state_name].config.announcements.announce_turn, styles.button + styles.float.right), announceRoundButton = makeButton(state[state_name].config.announcements.announce_round, '!' + state[state_name].config.command + ' config announcements announce_round|'+!state[state_name].config.announcements.announce_round, styles.button + styles.float.right), announceConditionsButton = makeButton(state[state_name].config.announcements.announce_conditions, '!' + state[state_name].config.command + ' config announcements announce_conditions|'+!state[state_name].config.announcements.announce_conditions, styles.button + styles.float.right), handleLongNameButton = makeButton(state[state_name].config.announcements.handleLongName, '!' + state[state_name].config.command + ' config announcements handleLongName|'+!state[state_name].config.announcements.handleLongName, styles.button + styles.float.right), useFXButton = makeButton(state[state_name].config.announcements.use_fx, '!' + state[state_name].config.command + ' config announcements use_fx|'+!state[state_name].config.announcements.use_fx, styles.button + styles.float.right), FXTypeButton = makeButton(state[state_name].config.announcements.fx_type, '!' + state[state_name].config.command + ' config announcements fx_type|?{Type|'+state[state_name].config.announcements.fx_type+'}', styles.button + styles.float.right), backButton = makeButton('&lt; Back', '!'+state[state_name].config.command + ' config', styles.button + styles.fullWidth), listItems = []; listItems.push('&lt;span style="'+styles.float.left+'"&gt;Announce Round:&lt;/span&gt; ' + announceRoundButton); listItems.push('&lt;span style="'+styles.float.left+'"&gt;Announce Turn:&lt;/span&gt; ' + announceTurnButton); if(!state[state_name].config.announcements.announce_turn){ listItems.push('&lt;span style="'+styles.float.left+'"&gt;Announce Conditions:&lt;/span&gt; ' + announceConditionsButton); } if(state[state_name].config.announcements.announce_turn){ listItems.push('&lt;span style="'+styles.float.left+'"&gt;Shorten Long Name:&lt;/span&gt; ' + handleLongNameButton); } listItems.push('&lt;span style="'+styles.float.left+'"&gt;Use FX:&lt;/span&gt; ' + useFXButton); if(state[state_name].config.announcements.use_fx){ listItems.push('&lt;span style="'+styles.float.left+'"&gt;FX Type:&lt;/span&gt; ' + FXTypeButton); } let contents = makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow)+'&lt;hr&gt;'+backButton; makeAndSendMenu(contents, script_name + ' Announcements Config', 'gm'); }, sendConfigTimerMenu = () =&gt; { let turnTimerButton = makeButton(state[state_name].config.timer.use_timer, '!' + state[state_name].config.command + ' config timer use_timer|'+!state[state_name].config.timer.use_timer, styles.button + styles.float.right), timeButton = makeButton(state[state_name].config.timer.time, '!' + state[state_name].config.command + ' config timer time|?{Time|'+state[state_name].config.timer.time+'}', styles.button + styles.float.right), chatTimerButton = makeButton(state[state_name].config.timer.chat_timer, '!' + state[state_name].config.command + ' config timer chat_timer|'+!state[state_name].config.timer.chat_timer, styles.button + styles.float.right), tokenTimerButton = makeButton(state[state_name].config.timer.token_timer, '!' + state[state_name].config.command + ' config timer token_timer|'+!state[state_name].config.timer.token_timer, styles.button + styles.float.right), tokenFontButton = makeButton(state[state_name].config.timer.token_font, '!' + state[state_name].config.command + ' config timer token_font|?{Font|Arial|Patrick Hand|Contrail|Light|Candal}', styles.button + styles.float.right), tokenFontSizeButton = makeButton(state[state_name].config.timer.token_font_size, '!' + state[state_name].config.command + ' config timer token_font_size|?{Font Size|'+state[state_name].config.timer.token_font_size+'}', styles.button + styles.float.right), backButton = makeButton('&lt; Back', '!'+state[state_name].config.command + ' config', styles.button + styles.fullWidth), listItems = [ '&lt;span style="'+styles.float.left+'"&gt;Turn Timer:&lt;/span&gt; ' + turnTimerButton, '&lt;span style="'+styles.float.left+'"&gt;Time:&lt;/span&gt; ' + timeButton, '&lt;span style="'+styles.float.left+'"&gt;Show in Chat:&lt;/span&gt; ' + chatTimerButton, '&lt;span style="'+styles.float.left+'"&gt;Show on Token:&lt;/span&gt; ' + tokenTimerButton, '&lt;span style="'+styles.float.left+'"&gt;Token Font:&lt;/span&gt; ' + tokenFontButton, '&lt;span style="'+styles.float.left+'"&gt;Token Font Size:&lt;/span&gt; ' + tokenFontSizeButton ]; let contents = makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow)+'&lt;hr&gt;'+backButton; makeAndSendMenu(contents, script_name + ' Timer Config', 'gm'); }, sendMenu = () =&gt; { let nextButton = makeButton('Next Turn', '!' + state[state_name].config.command + ' next b', styles.button), prevButton = makeButton('Prev. Turn', '!' + state[state_name].config.command + ' prev b', styles.button), startCombatButton = makeButton('Start Combat', '!' + state[state_name].config.command + ' start b', styles.button), stopCombatButton = makeButton('Stop Combat', '!' + state[state_name].config.command + ' stop b', styles.button), pauseTimerTitle = (paused) ? 'Start Timer' : 'Pause Timer', pauseTimerButton = makeButton(pauseTimerTitle, '!' + state[state_name].config.command + ' pt b', styles.button), stopTimerButton = makeButton('Stop Timer', '!' + state[state_name].config.command + ' st b', styles.button), addConditionButton = makeButton('Add Condition', '!' + state[state_name].config.command + ' add ?{Condition} ?{Duration}', styles.button), removeConditionButton = makeButton('Remove Condition', '!' + state[state_name].config.command + ' remove ?{Condition}', styles.button), resetConditionsButton = makeButton('Reset Conditions', '!'+state[state_name].config.command + ' reset conditions', styles.button), favoritesButton = makeButton('Favorite Conditions', '!'+state[state_name].config.command + ' favorites', styles.button), contents; if(inFight()){ contents = ' \ '+nextButton+prevButton+'&lt;br&gt; \ '+pauseTimerButton+stopTimerButton+' \ &lt;hr&gt; \ &lt;b&gt;With Selected:&lt;/b&gt;&lt;br&gt; \ '+addConditionButton+'&lt;br&gt; \ '+removeConditionButton+' \ &lt;hr&gt; \ '+favoritesButton+' \ &lt;hr&gt; \ '+stopCombatButton+'&lt;br&gt; \ '+resetConditionsButton; }else{ contents = ' \ '+startCombatButton+' \ &lt;hr&gt; \ '+favoritesButton; } makeAndSendMenu(contents, script_name + ' Menu', 'gm'); }, sendHelpMenu = () =&gt; { let configButton = makeButton('Config', '!' + state[state_name].config.command + ' config', styles.button + styles.fullWidth); let listItems = [ '&lt;span style="'+styles.underline+'"&gt;!'+state[state_name].config.command+' help&lt;/span&gt; - Shows this menu.', '&lt;span style="'+styles.underline+'"&gt;!'+state[state_name].config.command+' config&lt;/span&gt; - Shows the configuration menu.' ]; let contents = '&lt;b&gt;Commands:&lt;/b&gt;'+makeList(listItems, styles.reset + styles.list)+'&lt;hr&gt;'+configButton; makeAndSendMenu(contents, script_name + ' Help', 'gm'); }, makeAndSendMenu = (contents, title, whisper) =&gt; { title = (title &amp;&amp; title != '') ? makeTitle(title) : ''; whisper = (whisper &amp;&amp; whisper !== '') ? '/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; }, checkStatusInfo = () =&gt; { if(typeof StatusInfo === 'undefined'){ makeAndSendMenu('Consider installing '+makeButton('StatusInfo', '<a href="https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo" rel="nofollow">https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo</a>', styles.textButton)+' it works great with this script.', '', 'gm'); return; } if(!StatusInfo.version || StatusInfo.version !== "0.3.8"){ makeAndSendMenu('Please update '+makeButton('StatusInfo', '<a href="https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo" rel="nofollow">https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo</a>', styles.textButton)+' to the latest version.', '', 'gm'); return; } extensions.StatusInfo = true; }, checkInstall = () =&gt; { if(!_.has(state, state_name)){ state[state_name] = state[state_name] || {}; } setDefaults(); checkStatusInfo(); log(script_name + ' Ready! Command: !'+state[state_name].config.command); if(state[state_name].config.debug){ makeAndSendMenu(script_name + ' Ready! Debug On.', '', 'gm'); } }, handeIniativePageChange = (obj,prev) =&gt; { if(state[state_name].config.close_stop &amp;&amp; (obj.get('initiativepage') !== prev.initiativepage &amp;&amp; !obj.get('initiativepage'))){ stopCombat(); } }, 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:campaign:turnorder', handleTurnorderChange); on('change:campaign:initiativepage', handeIniativePageChange); on('change:graphic:top', handleGraphicMovement); on('change:graphic:left', handleGraphicMovement); on('change:graphic:layer', handleGraphicMovement); on('change:graphic:statusmarkers', handleStatusMarkerChange); if('undefined' !== typeof TokenMod &amp;&amp; TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(function(obj,prev){ handleStatusMarkerChange(obj,prev); }); } }, setDefaults = (reset) =&gt; { const defaults = { config: { command: 'ct', marker_img: '<a href="https://s3.amazonaws.com/files.d20.io/images/52550079/U-3U950B3wk_KRtspSPyuw/thumb.png?1524507826" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/52550079/U-3U950B3wk_KRtspSPyuw/thumb.png?1524507826</a>', throw_initiative: true, initiative_attribute_name: 'initiative_bonus', close_stop: true, pull: true, timer: { use_timer: true, time: 120, chat_timer: true, token_timer: true, token_font: 'Candal', token_font_size: 16, token_font_color: 'rgb(255, 0, 0)' }, announcements: { announce_conditions: false, announce_turn: true, announce_round: true, handleLongName: true, use_fx: false, fx_type: 'nova-holy' } }, conditions: {}, favorites: {} }; 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('marker_img')){ state[state_name].config.marker_img = defaults.config.marker_img; } if(!state[state_name].config.hasOwnProperty('throw_initiative')){ state[state_name].config.throw_initiative = defaults.config.throw_initiative; } if(!state[state_name].config.hasOwnProperty('initiative_attribute_name')){ state[state_name].config.initiative_attribute_name = defaults.config.initiative_attribute_name; } if(!state[state_name].config.hasOwnProperty('close_stop')){ state[state_name].config.close_stop = defaults.config.close_stop; } if(!state[state_name].config.hasOwnProperty('pull')){ state[state_name].config.pull = defaults.config.pull; } if(!state[state_name].config.hasOwnProperty('timer')){ state[state_name].config.timer = defaults.config.timer; }else{ if(!state[state_name].config.timer.hasOwnProperty('use_timer')){ state[state_name].config.timer.use_timer = defaults.config.timer.use_timer; } if(!state[state_name].config.timer.hasOwnProperty('time')){ state[state_name].config.timer.time = defaults.config.timer.time; } if(!state[state_name].config.timer.hasOwnProperty('chat_timer')){ state[state_name].config.timer.chat_timer = defaults.config.timer.chat_timer; } if(!state[state_name].config.timer.hasOwnProperty('token_timer')){ state[state_name].config.timer.token_timer = defaults.config.timer.token_timer; } if(!state[state_name].config.timer.hasOwnProperty('token_font')){ state[state_name].config.timer.token_font = defaults.config.timer.token_font; } if(!state[state_name].config.timer.hasOwnProperty('token_font_size')){ state[state_name].config.timer.token_font_size = defaults.config.timer.token_font_size; } if(!state[state_name].config.timer.hasOwnProperty('token_font_color')){ state[state_name].config.timer.token_font_color = defaults.config.timer.token_font_color; } } if(!state[state_name].config.hasOwnProperty('announcements')){ state[state_name].config.announcements = defaults.config.announcements; }else{ if(!state[state_name].config.announcements.hasOwnProperty('announce_turn')){ state[state_name].config.announcements.announce_turn = defaults.config.announcements.announce_turn; } if(!state[state_name].config.announcements.hasOwnProperty('announce_round')){ state[state_name].config.announcements.announce_round = defaults.config.announcements.announce_round; } if(!state[state_name].config.announcements.hasOwnProperty('announce_conditions')){ state[state_name].config.announcements.announce_conditions = defaults.config.announcements.announce_conditions; } if(!state[state_name].config.announcements.hasOwnProperty('handleLongName')){ state[state_name].config.announcements.handleLongName = defaults.config.announcements.handleLongName; } if(!state[state_name].config.announcements.hasOwnProperty('use_fx')){ state[state_name].config.announcements.use_fx = defaults.config.announcements.use_fx; } if(!state[state_name].config.announcements.hasOwnProperty('fx_type')){ state[state_name].config.announcements.fx_type = defaults.config.announcements.fx_type; } } } if(!state[state_name].hasOwnProperty('conditions')){ state[state_name].conditions = defaults.conditions; } if(!state[state_name].hasOwnProperty('favorites')){ state[state_name].favorites = defaults.favorites; } if(!state[state_name].config.hasOwnProperty('firsttime') &amp;&amp; !reset){ sendConfigMenu(true); state[state_name].config.firsttime = false; } }; return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers, ObserveTokenChange: observeTokenChange }; })(); on('ready',function() { 'use strict'; CombatTracker.CheckInstall(); CombatTracker.RegisterEventHandlers(); }); /* conditions = { xandir: [ { name: 'prone', duration: '1' } ] } */
When I have two conditions with timers, as soon as one is removed, the other skips a turn to count down.
1537326112
The Aaron
Pro
API Scripter
Just to be clear, is that with the modified copy I posted? &nbsp;If so, is that new behavior?
1537402068

Edited 1537402109
Yep. Just tested it, still having the issue.
1537405474

Edited 1537490054
Found the issue. In the getConditionString function, calling&nbsp; removeCondition(token, condition.name, true); before the forEach finished looping caused it to skip an item in the array. I fixed the issue by changing from a .forEach loop to a for() {} loop and decremented the index by 1 after removing the condition from the array. getConditionString = (token) =&gt; { let name = strip(token.get('name')).toLowerCase(); let conditionsSTR = ''; if(state[state_name].conditions[name] &amp;&amp; state[state_name].conditions[name].length){ for(let i = 0; i &lt; state[state_name].conditions[name].length; i++){ let condition = state[state_name].conditions[name][i]; if(typeof condition.duration === 'undefined' || condition.duration === false){ conditionsSTR += '&lt;strong&gt;'+condition.name+'&lt;/strong&gt;&lt;br&gt;'; }else if(condition.duration &lt;= 1){ conditionsSTR += '&lt;strong&gt;'+condition.name+'&lt;/strong&gt; removed.&lt;br&gt;'; removeCondition(token, condition.name, true); i--; } else { state[state_name].conditions[name][i].duration = parseInt(state[state_name].conditions[name][i].duration)+parseInt(condition.direction); conditionsSTR += '&lt;strong&gt;'+condition.name+'&lt;/strong&gt;: ' + condition.duration + '&lt;br&gt;'; } conditionsSTR += (condition.message) ? '&lt;em style="font-size: 10pt"&gt;'+condition.message+'&lt;/em&gt;&lt;br&gt;' : ''; } } return conditionsSTR; }, <a href="https://gist.github.com/sillvva/18550fe30c1dca4cfd9b6647ef66152c" rel="nofollow">https://gist.github.com/sillvva/18550fe30c1dca4cfd9b6647ef66152c</a>
1537447781
Victor B.
Pro
Sheet Author
API Scripter
@Sillvva, your link to Gist isn't working
1537450841

Edited 1537450877
Victor B.
Pro
Sheet Author
API Scripter
I've tested this a bit.&nbsp; The image outline that shows and follows the active player isn't showing.&nbsp; This is very helpful with lots of creatures that have the same tokens.&nbsp; This is combination of The Aaron's fixes plus the getConditionString from Sillvva
Hmm, try the link now.
1537508476

Edited 1537511213
I will test The Aaron's Script with Sillvva's fix. Thanks for the great support. Edit: I get another error, I do not know how I got it. TypeError: Cannot read property 'id' of null TypeError: Cannot read property 'id' of null at turnorder.forEach.turn (apiscript.js:4060:20) at Array.forEach (native) at checkMarkerturn (apiscript.js:4059:19) at getOrCreateMarker (apiscript.js:4027:9) at resetMarker (apiscript.js:3768:22) at startCombat (apiscript.js:3719:9) at handleInput (apiscript.js:3400:17) at eval (eval at &lt;anonymous&gt; (/home/node/d20-api-server/api.js:151:1), &lt;anonymous&gt;:65:16) at Object.publish (eval at &lt;anonymous&gt; (/home/node/d20-api-server/api.js:151:1), &lt;anonymous&gt;:70:8) at /home/node/d20-api-server/api.js:1634:12 Also I found that Tokens with shared name share condtions, e.g. Goblin 1(just named Goblin) is put to sleep for 5 rounds, then Goblin 2(also named Goblin) will also show the sleep condition and tick down the duration. This is an issue I found both in CT v.0.2. as well as in The Aaron's reworked script.
1537534832

Edited 1537534914
Victor B.
Pro
Sheet Author
API Scripter
I received the same error.&nbsp; It needed the prev object to be available within handleGraphicMovement within theAaron's code.&nbsp; I compared to original code, reset and it's working.&nbsp;&nbsp; I changed this: &nbsp; &nbsp; handleGraphicMovement = (obj/*, prev */)) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if(!inFight()) return; &nbsp; &nbsp; &nbsp; &nbsp; if(getCurrentTurn().id === obj.get('id')){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; changeMarker(obj); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }, to this: &nbsp; &nbsp; handleGraphicMovement = (obj, prev)) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if(!inFight()) return; &nbsp; &nbsp; &nbsp; &nbsp; if(getCurrentTurn().id === obj.get('id')){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; changeMarker(obj); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }, and it's working again