Editor’s note from OnlineDM: Today we are lucky to have another guest post, this one from Paul Baalham (@paulbaalham on Twitter; you can also find him at Daily Encounter).
Tracking ongoing damage and conditions with MapTool for 4E
As a DM that wants to use MapTool to handle all the boring stuff of D&D fights, to leave the players and me more tim for the fun stuff, I realised very early on that I wanted to create a system that would track ongoing damage and conditions so that we wouldn’t forget about the ongoing damage or that the slowed condition actually ended on the PC’s last turn. I managed to get the ongoing damage tracked quite easily so here is what I have done. Could it be improved? Almost certainly. If you can improve it then PLEASE let me know in the comments.
Campaign Properties
The code in this post require the following properties for the tokens that will be using it (i.e. players and monsters).
Acid:0 Cold:0 Fire:0 Force:0 Lightning:0 Necrotic:0 Poison:0 Psychic:0 Radiant:0 Thunder:0 Untyped:0 SEAcid:0 SECold:0 SEFire:0 SEForce:0 SELightning:0 SENecrotic:0 SEPoison:0 SEPsychic:0 SERadiant:0 SEThunder:0 SEUntyped:0 IsWarden:0
The SE variables are to help determine whether there is a Save Ends condition. I think there is probably a way of eliminating these, but I haven’t thought of a way yet (it works currently so I don’t want to break it!) The ISWarden may look out of place, but as Wardens can save against ongoing damage at the start of their turn, this variable helps in keeping track of the initiative.
The “Place Ongoing Damage” Button
I created a button for placing ongoing damage to tokens. This was situated on my Campaign macros window, but where you put it is up to you,
First of all, in the States section of the Campaign Properties the following states should be present:
Acid Cold Fire Force Lightning Necrotic Poison Psychic Radiant Thunder Untyped
Each one of these states should be in the Group called “Damage”.
This allows us to collect together all of the ongoing damage types like this:
[h: dmgList = getTokenStates(",","Damage")]
We then need to get a list of all of the tokens that are on the map:
[h: tokenList=getExposedTokenNames()] [h: imgList = tokenList] [h: Num = listCount(imgList)]
We need to create a list of token names and create a list of images to display.
[h,COUNT(Num),CODE: { [h:tokenName=listGet(imgList,roll.count)] [h,token(tokenName): image=getTokenImage()] [h:imgList=listReplace(imgList,roll.count,tokenName+" "+image)] }]
Next we display a pop up window that shows a list of targets (including images) as well as a list of the types of damage. Finally the value of the ongoing damage needs to be typed in by the DM.
[h:status=input( "Target|"+imgList+"|Select Target|LIST|SELECT=0 ICON=TRUE ICONSIZE=30", "damageType|"+dmgList+"|Select Type of Damage|LIST|SELECT=0 VALUE=STRING", "amount| |Enter amount of damage" )] [h:abort(status)]
We now know the name of the target so we can switch the focus to that token:
[h:targetName = listGet(tokenList,Target)] [h:switchToken(targetName)]
The following looks at what type of damage it was, sets the value of the appropriate variable and then creates a string to be dsiplayed at the end. This is the bit of code I think is the most likely that someone could improve upon.
[h,switch(damageType),code: case "Acid": { [h: Acid=amount] [h: SEAcid=1] [h: stringToShow= targetName+ " has ongoing Acid damage"] }; case "Cold": { [h: Cold=amount] [h: SECold=1] [h: stringToShow= targetName+ " has ongoing Cold damage"] }; case "Fire": { [h: Fire=amount] [h: SEFire=1] [h: stringToShow= targetName+ " has ongoing Fire damage"] }; case "Force": { [h: Force=amount] [h: SEForce=1] [h: stringToShow= targetName+ " has ongoing Force damage"] }; case "Lightning": { [h: Lightning=amount] [h: SELightning=1] [h: stringToShow= targetName+ " has ongoing Lightning damage"] }; case "Thunder": { [h: Thunder=amount] [h: SEThunder=1] [h: stringToShow= targetName+ " has ongoing Thunder damage"] }; case "Necrotic": { [h: Necrotic=amount] [h: SENecrotic=1] [h: stringToShow= targetName+ " has ongoing Necrotic damage"] }; case "Psychic": { [h: Psychic=amount] [h: SEPsychic=1] [h: stringToShow= targetName+ " has ongoing Psychic damage"] }; case "Poison": { [h: Poison=amount] [h: SEPoison=1] [h: stringToShow= targetName+ " has ongoing Poison damage"] }; case "Radiant": { [h: Radiant=amount] [h: SERadiant=1] [h: stringToShow= targetName+ " has ongoing Radiant damage"] }; case "Untyped": { [h: Untyped=amount] [h: SEUntyped=1] [h: stringToShow= targetName+ " has ongoing damage (untyped)"] } ]
Almost finished! We need to put the damage on the token.
[h:Condition=damageType] [h:setState(Condition,1)]
And finally show the string that we formatted earlier that tells everyone who has been tagged with ongoing damage, the type and how much.
[r: stringToShow]
Now we have a way of placing the ongoing damage on a token, we need a way of automating the subtraction of the damage from tokens at the start of their turn and reminding them to save at the end of their turn.
The “Next Initiative” Button
I created a button that I placed on the Campaign group called Next Initiative which passes the initiative to the next person on the list. But before it does this it checks to see if the current token has any save ends conditions on it (I have only pasted the ongoing damage here as conditions will be in another post).
First of all, we need to determine who the current token is and switch the focus to that token:
[h: id = getInitiativeToken()] [h: switchToken(id)] [h: targetname=getName(id)]
Next we need to see if the token has ongoing damage it needs to save against. It will then ask if the token has saved against it. The DM will see a popup window asking if the player saved against the condition. The player should still be able to roll for the save while the pop up is on the DM’s screen.
[h:saveList=""] [h,if(SEAcid), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Acid damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEAcid=0] [h:Acid=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Acid damage! :D"] [h:state.Acid=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Acid damage! :("] }] }] [h,if(SECold), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Cold damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SECold=0] [h:Cold=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Cold damage! :D"] [h:state.Cold=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Cold damage! :("] }] }] [h,if(SEFire), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Fire damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEFire=0] [h:Fire=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Fire damage! :D"] [h:state.Fire=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Fire damage! :("] }] }] [h,if(SEForce), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Force damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEForce=0] [h:Force=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Force damage! :D"] [h:state.Force=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Force damage! :("] }] }] [h,if(SELightning), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Lightning damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SELightning=0] [h:Lightning=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Lightning damage! :D"] [h:state.Lightning=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Lightning damage! :("] }] }] [h,if(SENecrotic), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Necrotic damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SENecrotic=0] [h:Necrotic=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Necrotic damage! :D"] [h:state.Necrotic=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Necrotic damage! :("] }] }] [h,if(SEPoison), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Poison damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEPoison=0] [h:Poison=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Poison damage! :D"] [h:state.Poison=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Poison damage! :("] }] }] [h,if(SEPsychic), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Psychic damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEPsychic=0] [h:Psychic=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Psychic damage! :D"] [h:state.Psychic=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Psychic damage! :("] }] }] [h,if(SERadiant), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Radiant damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SERadiant=0] [h:Radiant=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Radiant damage! :D"] [h:state.Radiant=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Radiant damage! :("] }] }] [h,if(SEThunder), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Thunder damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEThunder=0] [h:Thunder=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Thunder damage! :D"] [h:state.Thunder=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Thunder damage! :("] }] }] [h,if(SEUntyped), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Untyped damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEUntyped=0] [h:Untyped=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Untyped damage! :D"] [h:state.Untyped=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Untyped damage! :("] }] }] [h:stringToList(saveList, " ")] [h:number=listCount(saveList)] [r,if(number>0),code:{ [r,foreach(var,saveList),code:{ [r:var] <br> }] }] <br>
Pass the initiative onto the next token.
[h: nextInitiative()]
And here is where it starts to get a bit messy. My wife plays a Warden in the game. Wardens get to save against conditions at the START of their turn.
Get the token that now has initiative and switch the focus to them.
[h: id = getInitiativeToken()] [h: switchToken(id)]
If they are a Warden then repeat the code from up above:
[h,if(isWarden==1),code: { [h:saveList=""] [h,if(SEAcid), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Acid damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEAcid=0] [h:Acid=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Acid damage! :D"] [h:state.Acid=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Acid damage! :("] }] }] [h,if(SECold), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Cold damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SECold=0] [h:Cold=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Cold damage! :D"] [h:state.Cold=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Cold damage! :("] }] }] [h,if(SEFire), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Fire damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEFire=0] [h:Fire=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Fire damage! :D"] [h:state.Fire=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Fire damage! :("] }] }] [h,if(SEForce), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Force damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEForce=0] [h:Force=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Force damage! :D"] [h:state.Force=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Force damage! :("] }] }] [h,if(SELightning), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Lightning damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SELightning=0] [h:Lightning=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Lightning damage! :D"] [h:state.Lightning=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Lightning damage! :("] }] }] [h,if(SENecrotic), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Necrotic damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SENecrotic=0] [h:Necrotic=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Necrotic damage! :D"] [h:state.Necrotic=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Necrotic damage! :("] }] }] [h,if(SEPoison), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Poison damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEPoison=0] [h:Poison=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Poison damage! :D"] [h:state.Poison=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Poison damage! :("] }] }] [h,if(SEPsychic), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Psychic damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEPsychic=0] [h:Psychic=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Psychic damage! :D"] [h:state.Psychic=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Psychic damage! :("] }] }] [h,if(SERadiant), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Radiant damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SERadiant=0] [h:Radiant=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Radiant damage! :D"] [h:state.Radiant=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Radiant damage! :("] }] }] [h,if(SEThunder), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Thunder damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEThunder=0] [h:Thunder=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Thunder damage! :D"] [h:state.Thunder=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Thunder damage! :("] }] }] [h,if(SEUntyped), code: { [h:stringToShow="Did " + targetname + " Save against ongoing Untyped damage? (1=yes, 0=no)"] [h:status=input("hasSaved|0|"+stringToShow)] [h:abort(status)] [h,if(hasSaved), code: { [h:SEUntyped=0] [h:Untyped=0] [h:saveList=saveList+" ,"+targetname + " has saved against the ongoing Untyped damage! :D"] [h:state.Untyped=0] }; { [h:saveList=saveList+" ,"+targetname + " did not save against the ongoing Untyped damage! :("] }] }] }] [h:stringToList(saveList, " ")] [h:number=listCount(saveList)] [r,if(number>0),code:{ [r,foreach(var,saveList),code:{ [r:var] <br> }] }]
Next. we display the name of who has initiative and display the current conditions effecting them.
[r: getName(id) + " has Initiative"] [r:strlist = "and currently has"]
The next line just sees whether there is any ongoing damage at all (this could probably be used further up, but I haven’t done this yet).
[h:OD=Acid+Cold+Fire+Force+Lightning+Necrotic+Poison+Psychic+Radiant+Thunder+Untyped]
If there is ongoing damage then cycle through the damage types and subtract the value from the token’s HP.
[h,if(OD>0),code:{ [h,if(Acid>0):strlist=strlist+" ,"+ string(Acid)+" Acid damage"] [h,if(Cold>0):strlist=strlist+" ,"+ string(Cold)+" Cold damage"] [h,if(Fire>0):strlist=strlist+" ,"+ string(Fire)+" Fire damage"] [h,if(Force>0):strlist=strlist+" ,"+ string(Force)+" Force damage"] [h,if(Lightning>0):strlist=strlist+" ,"+ string(Lightning)+" Lightning damage"] [h,if(Necrotic>0):strlist=strlist+" ,"+ string(Necrotic)+" Necrotic damage"] [h,if(Poison>0):strlist=strlist + " ," + string(Poison)+" Poison damage"] [h,if(Psychic>0):strlist=strlist+" ,"+ string(Psychic)+" Psychic damage"] [h,if(Radiant>0):strlist=strlist+" ,"+ string(Radiant)+" Radiant damage"] [h,if(Thunder>0):strlist=strlist+" ,"+ string(Thunder)+" Thunder damage"] [h,if(Untyped>0):strlist=strlist+" ,"+ string(Untyped)+" Untyped damage"] [h:HP=HP-Cold] [h:HP=HP-Fire] [h:HP=HP-Force] [h:HP=HP-Lightning] [h:HP=HP-Necrotic] [h:HP=HP-Poison] [h:HP=HP-Psychic] [h:HP=HP-Radiant] [h:HP=HP-Thunder] [h:HP=HP-Untyped] [h:strlist=strlist + ", and now has " + HP + " HP"] [state.Dying = 1 - max(0,min(1,HP))] [state.Bloodied = 1 - max(0, min(1,HP - Bloodied))] [setBar("Health", HP/MaxHP)] }]
Finally display the ongoing damages that the token is suffering from.
[h:stringToList(strlist, " ")] [h:number=listCount(strlist)] [r,if(number>1),code:{ [r,foreach(var,strlist),code:{ [r:var] <br> }] }]
I hope someone finds this useful. I am not a software programmer by trade (although I do have to occasionally write code), so I am sure this code could be improved. It would be awesome if someone took this and made it better and allowed OnlineDM to post it on his site.
Very cool macro. What about the case where you have two different effects that deal ongoing damage of the same type? I believe they overlap, such that you take the highest damage effect, but still make 2 different saves. Definitely a rare case, and mostly wonder if it’s something I need to account for myself.
Hi Granger44,
Thanks for the comment. As I haven’t come across that situation yet, I never thought to try and accommodate it. 😮
Hmmm. I want to finish off the condition tracking first but I may come back this to see if I can modify it to cope with that (I already think I can improve the current code by removing lines here and there, but I want to get everything working before “improving it”).
Thanks again 🙂
Paul
Pingback: MapTool macros: Condition tracking for D&D4e « Online Dungeon Master