Building a Dungeons & Dragons battle tracker in vanilla JavaScript

I just finished Wes Bos’ ES6 for Everyone course and I’ve decided to try to test myself and what I’ve learned by building something somewhat useful.

I just started running a Dungeons & Dragons campaign for my kids and their friends. We had 10 people (kids and adults) playing the first session and we may have more coming. That’s a lot of people to keep track of generally, but especially when it comes to figuring out things like initiative, damage, etc. during fight scenes. And since we had one in the first session, trying to manage it on paper got to be pretty crazy.

There are mobile apps to manage this stuff, of course, but for that, you need to add a whole bunch of stats to even get it to work. And the one I’m thinking of (Game Master 5th Edition) is great, but it really has a lot more to it and is for running full on campaigns and isn’t so great with ad-hoc fights with random NPCs you hadn’t previously added to the system.

So, I decided to try to build a tool that would do a couple things:

  • manage all the characters I need to keep track of, including NPCs
  • allow me to enter in (manually) the initiative rolls and add the bonus for the characters automatically
  • sort them by their initiative
  • track their health over the course of the battle
  • allow the initiatives and damage to be reset at the end of the fight

I’ve thrown together some code and decided to document this as I go, which means this post will be a little stream-of-consciousness. The first part was building a boring HTML file with some really basic elements that I can start adding more elements to dynamically with JavaScript. I started out dumping all my js in the html file, but ultimately split it out into a separate js file. So the HTML file just looks like this:

The idea is that when you click the Add New button, you can add a new character. You’ll an input for the character in the unordered list and then you can Add that character to the list. You will be able to add any number of characters.

To start, I need to set some assumptions. I created some variables to identify the core pieces of this markup that I will be working with, namely the character-list itself and the add-new-character button. After playing around with some different things, I ultimately built out a function to add an input (for the character name) and an Add button and insert those into the character-list. I then bound this to an event listener on the “click” event on the “Add new” button.

At this point, I have a button that adds new elements to an unordered list. Each list item has an input field and another button. Now what I need is a way to “save” the information (in this case, character names). And I’ll need to add another event listener inside the event listener that’s hooked to the “Add new” button, because the list items don’t exist until you click that button. You’ll notice that I have some logic to keep track of a character count — this wasn’t actually in the first pass. This is so I can uniquely identify each new character by an id and tie together the input and the button and the list item. This is handled by a function that will extract the numeric value out of any element id.

With the getId function, I have everything I need to get rid of the inputs, replace them with a text node and “save” (at least on the page) a new character name. Here’s what the updated event listener looks like:

And this is a gif of it in action:

Animation of early iteration of battle tracker functionality.

I’m pretty happy with this but there’s a few more things I need.

First of all, I need to determine a character’s initiative and their initiative bonus. Initiative comes from a d20 dice roll with their initiative bonus added to that result. The bonus comes from the character’s Dexterity bonus. Now, one thing I could do is just add an input field just for the initiative bonus. But one thing I constantly struggle with as I’m making and working with characters is remembering the bonus table — e.g. what the bonuses (or negatives) are in relation to what ability scores. It might be handy to have a generic input for dexterity and then calculate the bonus for you. We’ll also need an input field for initiative.

Being from a PHP background, using a switch makes the most sense to me, even though it’s more complex, because there are a lot of conditions where the ability score modifiers don’t just follow a linear progression. I’ve built a function to take an ability score (from 1 – 30) and return the modifier for that score.

Ultimately, we’ll use these to take an input Dexterity score and add the modifier to an input initiative roll. So let’s take care of those next. First I need to add an input for Dexterity. Since initially, the Add button disappears when a name is added, I added some somewhat complex logic to handle what happens if a name but not a dex score is added and vice versa. The end result is, the button stays, but the button text changes to reflect the thing that needs to be added. This will be helpful for validation later, if I add that.

I split all this functionality out into its own function that gets fired within the initial event handler for the Add New button.

I realized that the Dexterity input field should be numeric, and probably should have a specific range to support realistic numbers only (e.g. 1 – 30), so I went back in and tweaked the initial function that builds out those inputs to add a couple attributes to that input. In doing so, I realized that now the placeholder text is too large to be entirely visible. The size attribute doesn’t work on numeric input fields, so I can’t make it larger that way. Since I plan to apply a layer of CSS to this at the end, I’ll need some classes, too, to identify certain types of inputs. So, I went back to add some classes to those. element.classList.add() still feels new to me, having only previously done this with jQuery and $('element').addClass(), but it doesn’t feel any harder and literally accomplishes the same task.

So now my rendered inputs look like this initially:

And they look like this after they’ve gotten values added to them:

This gives me something to work with, styling-wise. Moving on, I realized that the initiative bonus value isn’t actually being stored anywhere after it’s calculated. I’ll add a data attribute to the span so I can pull that out later to do the math for initiative totals. I’ll also need to add another input after the character is listed for that character’s initiative. At some point, I’ll need to add a separate section for enemies/NPCs, and then I’ll want to order all of them together by initiative value.

After taking care of the data attribute, by again making use of element.addAttribute(), which is quickly becoming my most-used function, I have something that looks like this after Dexterity scores are calculated:

So far, so good. Now let’s add another input for initiative after the character is added, then move on to NPCs. I’ve been wondering if using promises might be useful here — to wait for all the data to get back before doing…something. I don’t really think it’s applicable because we’re not fetching from a remote API and, while there are a number of things that will be rendered eventually it’s all user-input, so it seems like it doesn’t make sense in this context. However, the entire process is all about chaining functions — do this, then do this, then do this — which is something that promises let you do. In the meantime, I’m literally adding a call to the next function within the previous function. I guess that works.

The other thing on my mind is adding in Webpack to handle a build process and render SaSS when I start adding styles and then throwing the whole project on GitHub. Ultimately that will change the architecture of the project, but that will come later…

Handling initiative means we need to do some math — we need to take the initiative roll and add (or subtract) the initiative modifier. At the same time that we add the initiative roll, I’m adding an input to record HP (hit points). First I need to add the initiative input. I add this to the end of the function that handles the Dex and character name, and the initiative input only displays if we have values in both of those fields.

This, then adds another event listener to the new “Save Initiative roll” button which calculates the initiative value and passes it along to another function which saves it to a data attribute. At the same time, I also add a field for managing Hit Points.

Now that I’ve got an HP field, I might as well manage hit points. I’ve added a function that not only stores the max hit points based on the initial value added, but also tracks the current and last hit point values. This is so I can display a message that says something happened.

If a character is reduced to 0 HP, we record that they died and remove the Update HP button.

Initially, having to click the Update button twice after entering 0 seemed like a bug, but in retrospect, having that level of “are you sure” actually seemed like a good thing for a DM — it allows the possibility to come back via death saving throws or spells that prevent a character from dying. So I actually added a condition that built on that and added an additional message for resurrections.

One thing that this tracker doesn’t do is prevent characters from going above their max HP. There’s a whole rabbit hole that I could go down if I start taking into consideration “temporary hit point” or other things outside a hard and fast HP value, but if we’re saving max HP, we should probably prevent the HP from going above that. It also seems a bit counter-intuitive as I’m using this to update the HP total as opposed to entering the amount of damage dealt. That means I need to do the math in my head first rather than letting the tracker take care of that. I might come back to that problem…

Handling not going above a character’s max HP is pretty simple — it’s just a condition if the input value is greater than the max HP data attribute value, and if it is, we set the input value to the max HP. This looks like this when it comes up:

To add monsters and NPCs, I can just use all the existing functions and event listeners used for characters. I just need to attach the initial click handler to a different button in a new section. That makes everything super-easy, though, because the code is already written and I know it all works and is tied specifically to character IDs which are unique to the element that’s added.

The last thing I need to do is add a way to sort characters and NPCs by their initiative values. And in doing this, I need to identify which characters are player characters and which characters are non-player characters and monsters.

My initial thought was that I’d literally just take the existing list(s) and re-sort them based on initiative. However, on reflection, maybe I should create a new, separate list that renders once when everyone is added, in the order they should be. And then maybe I could add an input field to track the actual amount of damage dealt to the character rather than having to enter the current total HP — then the HP field just updates automatically or is removed and replaced by just text. Since I have the input, I think I’ll leave it but maybe make it disabled so it can’t be edited manually…

First, though, I need to create a new list for the actual initiative order. But this list shouldn’t be displayed until we have all the characters and their initiatives calculated. Whether it’s rendered as an empty list on the page first or added to the DOM by JavaScript doesn’t matter too much, but I generally like the pattern of adding the empty element to the page and then filling it in later.

In order to know when we’re done adding characters, we need some sort of “save” button. I’ve added a button that says “Let’s go! 💥” that will act as the start of battle trigger. However, this should not be active all the time — only when we have characters. And if we have an incomplete list of characters, we shouldn’t be able to start a fight. I’ve built out a button, event listener, and a couple functions that check for contents in the two lists (character and NPC/monsters) and if one or the other of those are empty it displays a message saying you’re not done, so you understand why clicking the button doesn’t do anything. The buttons and messages go away when there’s at least a single PC and a single NPC.

I’ve removed the description that appears below the button in the code after this gif was taken…

That gets us all ready for creating:

  • a list of characters and NPCs/monsters
  • …ordered by initiative
  • …with an input for damage taken

After creating the list with the damage input, I’ll remove the update HP button and do a bit of refactoring for the messages so they are triggered differently.

First we need to figure out the data for each character that we need to pass and track and the best way to store that is in an array of objects. Then we can sort these character objects by initiative and loop over them to add them to the list. I have a function that will scrap the page for all individual characters and return the data we want as an array of objects that we can work with.

Now we can use that information to fill in a list for the initiative order and damage tracking.

The recordCharacterDamage function doesn’t exist yet, but that’s what I’ll build next. Essentially, what that’s going to do is replace the earlier functionality I built in that triggers on the “Update Hit Points” button (which I’ve removed). The actual functionality of the triggered event will remain mostly the same, which is displaying a message and updating the HP, the difference being that I’ll also want to update the initiative order. If a character dies, we’ll want to display that somehow in the list of characters.

At this point, we can build a list of characters. Here’s an early look of what that looks like. The code snippet above includes their initiative score in the line, but I added that after I took this screenshot…

I wanted to visually indicate whether a character was a player character or non-player character, so I’ve added that as a data attribute that gets stored that I pull out when building the initiative list so we can see that visually. And I thought it was nice to be able to see what each character’s initiative was, even though it’s not absolutely essential. Next I need to refactor damage tracking so it’s triggered by the new “Record Damage” button rather than the (now non-existent) “Update Hit Points” button.

Now I need to refactor how damage is recorded. I’m not going to detail all this because it is a lot of what was already built, just in a different order. The tl:dr; is I broke out a separate function for initially establishing the hit points, and then I used the existing updateHp function to deal with recording and updating that. After getting that working, I was left with some duplicate code updating data attributes, because now I want to track them in the initiative list rather than on the initial character and monster list items. As long as the input value gets updated, we can pull that value in as the current HP and everything is fine, so we don’t need to update the data attributes after the first time things are set up.

Now we have a working damage tracking system and a short gif of it in action is below. It’s ugly as sin and I want to make it pretty so the next step will be pulling in some build libraries to handle SaSS and minify the js. I’ve posted the code on GitHub and I’ve created a pre-release at this checkpoint. You’re free to check it out, comment, and suggest pull requests if there are things you see that could be handled better.

I’ll write a followup post when I start working on making the front-end more presentable.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.