Building a Dungeons & Dragons battle tracker in vanilla JavaScript

This entry is part 1 of 1 in the series Dungeons and Dragons Battle Tracker js project

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.

Some words about Charlottesville, white privilege and racism

I have many words and feelings I want to express, but before I do, watch this, if you haven’t already. It’s important to witness what happened in Charlottesville and who these people are. The reporters at VICE who documented this are incredibly brave and deserve a medal.

The opposite of alt-right isn’t alt-left.
The opposite of Nazi-ism isn’t Communism.
The opposite of white supremacy isn’t white hatred.

I was a punk rock kid in high school. Punk is very much the music of teenage rebellion, of pushing boundaries, of clashing with the system. It’s also an identity thing — punk is about not caring about other people’s expectations of what you should be, it’s about making those choices yourself, no matter how brash, how loud and how stupid.

I bring this up because I was remembering a Black Flag song yesterday, “White Minority”. At the time, I assumed it was sarcastic, because I was 90% sure Black Flag wasn’t racist (they had a Puerto Rican singer for a while, after all). But the lyrics aren’t clear on that point:

We’re gonna be a white minority
We won’t listen to the majority
We’re gonna feel inferiority
We’re gonna be white minority

White pride You’re an American
I’m gonna hide Anywhere I can

I knew a couple “white pride” punk kids in high school. It’s one step away from the skinhead punks I’d see hanging around Haight-Ashbury in San Francisco. It’s one step away from the “white nationalists” in the VICE video. In fact, honestly, I can’t even say it’s a step away, really.

At the time of the song, lots of people thought it was racist for what I’d see as fairly obvious reasons. But Greg Ginn, the founder of Black Flag and author of the song, said no.

The idea behind it is to take somebody that thinks in terms of “White Minority” as being afraid of that, and make them look as outrageously stupid as possible. The fact that we had a Puerto Rican (Ron) singing it was what made the sarcasm of it obvious to me. Some people seem to want to take it another way, and somehow think that we’d be so dumb to where a Puerto Rican guy would sing it and it would be–I don’t know how they could consider that racist, but people took it that way.

But in the same interview, he says later that he really doesn’t care that much about that song, in particular.

It’s not a kind of song that has a long term emotional impact or value to us. We don’t even play it all the time.

This is the kind of careless use of words and language that comes with white privilege. Greg Ginn never experienced racism first-hand, but you can bet his Puerto Rican singer, Ron Reyes did. He never thought seriously about skinheads and Nazi punks taking his song and using it as an anthem to promote white supremacy and ethnic cleansing and thought it was “obvious” that it was satire, that he’s making fun of those guys. But if you put an angry, weight-lifting white dude with a shaved head like Henry Rollins in front of the band, it takes on a different meaning and the satire becomes (even) less obvious.

Words have meaning. Words matter and the choice of words matter. This is why we are angry about what Donald Trump said, and didn’t say and then said after coercion and then went back on again with regard to Charlottesville.  The intended meaning of words is irrelevant: how words are interpreted is what matters.

Apologizing or explaining later “oh, that’s not what I meant” doesn’t change anything if people use those same words to incite violence. Donald Trump’s words are being interpreted very favorably among white supremacists whether he knows it, or cares, or not. And that’s what brought us here.

I am pissed.

I dreamt about Nazis last night. I had a Twitter rant in my head the other day that I didn’t write down because I start to form thoughts together and something new happens and I’m having that thing again that happened at the beginning of the election where every minute it’s some new, horrible thing, and I can’t stop hitting refresh.

I have been ashamed by the religion I was born into.
I have been ashamed of the gender I was assigned at birth.
I have been ashamed of my color.
But until this year, I have never been ashamed of my nation.

We have literally gone to war to defeat Nazi ideology, to defeat the concept of a “superior race” and now we have an American President who defends people who agree with those things.

I remember this sketch from Saturday Night Live after the election where a group of white people and the token black character are watching the election results come in.

This is literally us right now. How did this happen? You weren’t paying attention. This has been happening for a long time and we sat there, being complacent and thinking all our victories were won when we elected our first black president.

I’m not perfect either. At the rally for solidarity I went to Monday night, organized by the Utah League of Native American Voters, three people, dressed all in black, wearing masks and carrying a flag I couldn’t read entirely came up behind us and I was nervous. There were counter-protests happening and I wasn’t entirely convinced that they weren’t trying to infiltrate the rally somehow. It took me a minute to realize they were latinx. And even after that, and after realizing they were applauding the speakers, my unease did not entirely lift right away. It wasn’t until we were walking back to our car after the rally that it hit me — they were most likely covering their faces for protection. The same reason many people were saying it’s dumb that the white supremacists didn’t wear masks Friday night (and are now suffering the consequences), and the reason the original KKK wore hoods: to protect their identities. If they were undocumented immigrants, they could be deported. Hell, in this country right now, even if they weren’t undocumented they could get deported. And again, I’m struck by my white privilege.  And my ingrained racism.

It’s nice to say “I don’t see color” but that’s like saying “I don’t see gender” or even “I don’t see flowers.” Unless you’re actually blind, you see color. It affects you. Maybe you don’t allow it to affect you, maybe you are fighting with all your might for it not to affect you, maybe you really, really don’t want it to affect you, but it does. And the sooner you can acknowledge that, the sooner you can be able to recognize that this is not a new problem. This is a 200-year-old problem.

I used to believe that in order to be a good person, I needed to be tolerant of everyone, even if I don’t agree with them. This extended, I believed, to people using the power of freedom of speech to spout hatred and intolerance. “That’s their right, I can disagree, but I can’t take it away.” While it’s true that I can’t take away another person’s freedom to express themselves, remaining silent is condoning that behavior. I learned about the paradox of tolerance, a philosophical concept that says this:

Unlimited tolerance must lead to the disappearance of tolerance. If we extend unlimited tolerance even to those who are intolerant, if we are not prepared to defend a tolerant society against the onslaught of the intolerant, then the tolerant will be destroyed, and tolerance with them.

Remaining silent is the same as allowing intolerance to take hold. It’s easy, and it’s easy to justify under the banner of “free speech” but it is just as violent and  just as destructive as driving a car into a line of protestors.

It strikes us, as Americans, as harsh to hear that in Germany you can get arrested for making the Nazi salute. Our knee-jerk reaction is that that’s a civil rights violation. But the Germans know what happens if that kind of thing goes unchecked. They know how easily it starts and how quickly it can spread to hysteria and get out of control. And maybe that doesn’t even entirely solve the problem, but it sets a precedent: these ideas are not welcome here.

I don’t have any solutions and there are many problems. I am just as tired as you are. I am read the news by white men on TV. I am represented in government by white men. People I follow on Twitter, friends of mine on Facebook, speakers I bring to WordCamp are white men (thankfully not all of them). But I am listening. And I will continue to listen. And I will try to use my privilege to fight.

When there was news of a counter protest at the rally Monday night, a lot of people said they wouldn’t go. There was a threat of violence, some white supremacists taking pictures of guns and threatening to bring them. I went anyway. Because, you know what? Living in constant fear of being attacked is what it means to be a person of color in this country. And until that changes, we need to do everything we can to shape this country to match our beliefs and the ideals that we are all created equal, every one of us, and that we are indivisible.

The thing that has always given me the most pride in my nation has been the thing I learned about in elementary school — that the United States is a melting pot. Everyone comes from somewhere else, and it’s when we bring all those people together and mix up all those ideas and beliefs and values that we create something that is ours. To be American is not to be white. It is to be multi-cultural. We need to build bridges not tear them down.

Shepard Fairey We the People Defend Dignity Shepard Fairey We the People are Stronger Than Fear Shepard Fairey We the People Protect Each Other