A Simple Tower Defense Game in Javascript

Been playing around with Javascript the last two days, and wrote up a simple tower defense game. If you aren't familiar with tower defense, have a quick read of this to familiarize yourself with the genre.

You can play the completed version of this game here. I haven't used any external images to complete this game, just pure CSS, HTML and Javascript. Playing around with the game will help you understand the post below, so give it a try.

First we will get the HTML out of the way:

  1.  
  2.  
  3. Tower Defense
  4.  
  5. <script src="towerdefense.js" type="text/javascript"><!--mce:0--></script>
  6.  
  7.  

This makes up index.html for our example. Nothing much going on in the HTML at all, everything will be handled by the javascript and style sheet.

Function handlers

Before we can do anything we need to define some boilerplate event handlers.

  1. function listenEvent(eventTarget,eventType,eventHandler)
  2. {
  3. if(eventTarget.addEventListener)
  4. {
  5. eventTarget.addEventListener(eventType,eventHandler,false);
  6. }
  7. else if(eventTarget.attachEvent)
  8. {
  9. eventType = "on" + eventType;
  10. eventTarget.attachEvent(eventType,eventHandler);
  11. }
  12. else
  13. {
  14. eventTarget["on" + eventType] = eventHandler;
  15. }
  16. }
  17.  

Listen event provides a cross browser way of adding event handlers to your objects. An event handler, such as "onclick" is used to call a function when an event is triggered, such as someone clicking a control on the page. This function checks if addEventListener (the latest and preferred method of adding events) exists. If it doesn't it drops back to earlier equivalents on the same function.

  1. function cancelEvent(event)
  2. {
  3. if(event.preventDefault)
  4. {
  5. event.preventDefault();
  6. }
  7. else
  8. {
  9. event.returnValue = false;
  10. }
  11. }
  12.  
  13. function cancelPropogation(event)
  14. {
  15. if(event.stopPropogation)
  16. {
  17. event.stopPropogation();
  18. }
  19. else
  20. {
  21. event.cancelBubble = true;
  22. }
  23. }
  24.  

cancelEvent and cancelPropogation are used in the drag and drop code. This stops the events from reaching any higher elements in the code, as we want to handle the drag and drop behavior ourselves.

Drawing the form

We want to draw up the form to look like the following:

We start off by defining the control buttons, start and reset. The CSS for these buttons is as follows:

  1.  
  2. .startbutton
  3. {
  4. background-color: #4682B4;
  5. position: absolute;
  6. width: 100px; height: 50px;
  7. color: #FFFFFF;
  8. text-align: center;
  9. -moz-border-radius: 15px;
  10. -webkit-border-radius: 15px;
  11. top: 495px; left: 20px;
  12. }
  13.  
  14. .resetbutton
  15. {
  16. background-color: #4682B4;
  17. position: absolute;
  18. width: 100px; height: 50px;
  19. color: #FFFFFF;
  20. text-align: center;
  21. -moz-border-radius: 15px;
  22. -webkit-border-radius: 15px;
  23. top: 560px; left: 20px;
  24. }
  25.  

The -moz-border-radius/-webkit-border-radius flags give the buttons their rounded look. We have used absolute positioning to place the buttons at the right spot on the screen. An improvement to this is when the map is drawn we recalculate the position of the buttons, but that is left as an exercise to the reader.

The map itself is going to built up of a grid of <div> elements. The style of these elements is as follows:

  1.  
  2. .mapzone
  3. {
  4. background-color: #228B22;
  5. position: absolute;
  6. top: 0px; left: 0px;
  7. width: 15px; height: 15px;
  8. }
  9.  

All the map tiles are originally the green color we see in the picture, and the javascript will change the tiles of the path as required.

The last thing we need are the turret creation buttons. Their style is given here:
.turret
{
background-color: #C0C0C0;
opacity: 0.7;
border: 2px solid #000000;
position: absolute;
top: 500px; left: 0px;
width: 40px; height: 40px;
color: #000000;
text-align: center;
-moz-border-radius: 20px;
-webkit-border-radius: 20px;
}

We set the opacity element to 0.7, as they are initially disabled. If you have enough cash to purchase a turret, the opacity becomes 1 as the button is fully visible.

Lets have a look at the javascript to draw the map:

  1.  
  2. function drawMap()
  3. {
  4. // create the map zone
  5. for(var j = 0; j < MAP_H; j++)
  6. {
  7. for(var i = 0; i < MAP_W; i++)
  8. {
  9. var mapzone = document.createElement("div");
  10. mapzone.setAttribute("id","mapzone"+i);
  11. mapzone.setAttribute("class","mapzone");
  12. mapzone.style.left = TILE_H*i + "px";
  13. mapzone.style.top = TILE_W*j + "px";
  14. if(level1(i,j))
  15. {
  16. mapzone.style.backgroundColor = "#1E90FF";
  17. }
  18. else
  19. {
  20. // if it isn't part of the map, its a drop target for a turret
  21. listenEvent(mapzone,"dragenter",cancelEvent);
  22. listenEvent(mapzone,"dragover",dragOver);
  23. listenEvent(mapzone,"drop",mapDrop(mapzone));
  24. }
  25. document.body.appendChild(mapzone);
  26. }
  27. }
  28.  

MAP_H and MAP_W are global constants representing the width and height of the map in terms of tiles. TILE_H and TILE_W are the size of the individual tiles themselves. They are defined as follows:

  1.  
  2. const TILE_H = 15;
  3. const TILE_W = 15;
  4. const MAP_H = 30;
  5. const MAP_W = 80;
  6.  

These can be configured to change the size of the map. Each tile is a <div> element and is assigned the class "mapzone" to match the CSS definition.

Map tiles can be two things:

  • The path the minions travel on
  • Place where the turrets can be placed

Hence we have an if statement here. We check a function called level1, which defines the path the minions take.

  1.  
  2. function level1(i,j)
  3. {
  4. if( (i == 0 && (j >= 0 && j <= 2))
  5. || (j == 2 && (i >=0 && i < 70))
  6. || (i == 70 && (j >=2 && j <= 28))
  7. || (j == 28 && (i <= 70 && i >= 60))
  8. || (i == 60 && (j <= 28 && j >= 5))
  9. || (j == 5 && (i <= 60 && i >= 40))
  10. || (i == 40 && (j >= 5 && j <= 25))
  11. || (j == 25 && (i <= 40 && i >= 30))
  12. || (i == 30 && (j >= 20 && j <= 25))
  13. || (j == 20 && (i <= 30 && i >= 5))
  14. || (i == 5 && (j <= 20 && j >= 10))
  15. || (j == 10 && (i >= 5 && i <= 80))
  16. )
  17. {
  18. return true;
  19. }
  20. return false;
  21. }
  22.  

Pretty terrible code, but does its job. Basically just determine the ranges for x and y coords in tiles, and if the current tile being placed down is in that range, it becomes the path. I called the function level1 in the hopes that perhaps in the future there would be level2, level3 ... leveln for different maps. A note if you are extending this code the path finding algorithm assumes the minions start from (0,0), so other levels will have to do the same, unless you want to rewrite that as well.

If the tile isn't part of the level, then it needs to be declared as a drop target. We set up event handlers to handle three events: dragenter, when a drag and drop object first encounters a drop target, dragover when the object is dragged over and drop if a draggable item is dropped on the target. We will discuss drag and drop handlers below.

We then add the map tiles to the document root and the map now appears!

  1.  
  2. // create the turrets
  3. for(var k = 0; k < 5; k++)
  4. {
  5. var turret = document.createElement("div");
  6. turret.setAttribute("id","turret"+k);
  7. turret.setAttribute("class","turret");
  8. turret.style.left = TURRET_OFFSET + (TURRET_D + TURRET_GAP)*k + "px";
  9. turret.style.borderColor = turretColor(turret.id);
  10. turret.innerHTML = "<p>" + k + "<br /><br />$" + turretValue(turret.id) + "</p>";
  11.  
  12. // turrets are draggable
  13. listenEvent(turret,"click",turretClick(turret));
  14. document.body.appendChild(turret);
  15. }
  16.  

Here we create the turret buttons to generate new turrets to be dragged and dropped. Once again they are <div> objects of class turret. The constants are defined as follows:

  1.  
  2. const TURRET_OFFSET = 200;
  3. const TURRET_GAP = 5;
  4. const TURRET_D = 40;
  5.  

We add a click handler to each turret button which will produce a draggable turret to be placed on the map. We will discuss the function turretClick below.

turretColor and turretValue are simple helper functions to return the color and the cost of each turret.

  1.  
  2. function turretColor(turretID)
  3. {
  4. switch(turretID)
  5. {
  6. case "turret0":
  7. return "#DDA0DD";
  8. case "turret1":
  9. return "#0000FF";
  10. case "turret2":
  11. return "#008080";
  12. case "turret3":
  13. return "#FF4500";
  14. case "turret4":
  15. return "#FF0000";
  16. }
  17. }
  18.  
  19. function turretValue(turretID)
  20. {
  21. switch(turretID)
  22. {
  23. case "turret0":
  24. return 10;
  25. case "turret1":
  26. return 100;
  27. case "turret2":
  28. return 500;
  29. case "turret3":
  30. return 1000;
  31. case "turret4":
  32. return 5000;
  33. }
  34. }
  35.  

The cost values probably still need some tuning, I haven't done extensive playtesting. Any feed back at all is helpful!

  1.  
  2. // put a start button on
  3. var startbutton = document.createElement("div");
  4. startbutton.setAttribute("id","startbutton");
  5. startbutton.setAttribute("class","startbutton");
  6. startbutton.innerHTML = "<p> Start! </p>";
  7. listenEvent(startbutton,"click",startwave);
  8. document.body.appendChild(startbutton);
  9.  
  10. // reset button
  11. var resetbutton = document.createElement("div");
  12. resetbutton.setAttribute("id","resetbutton");
  13. resetbutton.setAttribute("class","resetbutton");
  14. resetbutton.innerHTML = "<p> Reset </p>";
  15. listenEvent(resetbutton,"click",resetwave);
  16. document.body.appendChild(resetbutton);
  17.  
  18. // status bar
  19. var statusbar = document.createElement("div");
  20. statusbar.setAttribute("id","statusbar");
  21. statusbar.setAttribute("class","statusbar");
  22. statusbar.innerHTML = '<p> Cash: <span id="cash">$0</span> Score: <span id="score">0</span> Wave: <span id="wave">0</span> Lives: <span id="lives">0</span></p>';
  23. document.body.appendChild(statusbar);
  24. }
  25.  

Here we set up the Start Button, the Reset button and the Status bar and add event handlers. The status bar will display all the player information.

Building turrets

Above we saw how we created turrets that had a click handler. Here is the code to that handler:

  1.  
  2. function turretClick(turret)
  3. {
  4. function tclick(evt)
  5. {
  6. if(!isRunning || isPaused)
  7. {
  8. return;
  9. }
  10.  
  11. // do we have enough money to make a tower?
  12. if(currentCash < turretValue(turret.id))
  13. {
  14. return;
  15. }
  16.  
  17. evt = evt || window.evt;
  18.  
  19. // find out the window coordinates
  20. var x = 0; var y = 0;
  21.  
  22. if(evt.pageX)
  23. {
  24. x = evt.pageX;
  25. y = evt.pageY;
  26. }
  27. else if(evt.clientX)
  28. {
  29. var offsetX = 0; var offsetY = 0;
  30. if(document.documentElement.scrollLeft)
  31. {
  32. offsetX = document.documentElement.scrollLeft;
  33. offsetY = document.documentElement.scrollTop;
  34. }
  35. else if(document.body)
  36. {
  37. offsetX = document.body.scrollLeft;
  38. offsetY = document.body.scrollTop;
  39. }
  40. x = evt.clientX + offsetX;
  41. y = evt.clientY + offsetY;
  42. }
  43.  
  44. // create a new shaped turret at the mouse coords
  45. var turretD = document.createElement("div");
  46. turretD.setAttribute("id",turret.id + ":" + turretDragCounter++);
  47. turretD.setAttribute("class","turretdrag");
  48. turretD.style.left = x + "px";
  49. turretD.style.top = y + "px";
  50. turretD.style.backgroundColor = turretColor(turret.id);
  51. turretD.setAttribute("draggable","true");
  52. listenEvent(turretD,"dragstart",turretDrag(turretD));
  53. document.body.appendChild(turretD);
  54. // reduce our available cash by what we just spent
  55. currentCash -= turretValue(turret.id);
  56. }
  57. return tclick;
  58. }
  59.  

turretClick takes a turret object and returns a tclick function. tclick is the actual event handler, however we needed some properties of the turret that was clicked to produce the draggable turret.

First we start with what the draggrable turret will look like

  1.  
  2. .turretdrag
  3. {
  4. background-color: #FFFFFF;
  5. position: absolute;
  6. width: 15px; height: 15px;
  7. -moz-border-radius: 8px;
  8. -webkit-border-radius: 8px;
  9. font-size: 15px;
  10. text-align: center;
  11. top: 0px; left: 0px;
  12. }
  13. *[draggable=true]
  14. {
  15. -moz-user-select:none;
  16. -khtml-user-drag: element;
  17. cursor: move;
  18. }
  19. :-khtml-drag
  20. {
  21. background-color: rgba(238,238,238,238,0.5);
  22. }
  23.  

turredrag is the <div> class that defines what the turret actually looks like - in this case a small circle the size of a map tile. The *[draggable=true] style defines the object as something that is draggable and sets the appropriate css flags. -khtml-drag defines what it looks like when being dragged - here we make it slightly translucent while the object is being dragged.

Now to the javascript. First we check if we are running or in pause mode. These are globals defined as such:

  1.  
  2. var isRunning = false;
  3. var isPaused = false;
  4.  

They are controlled by pressing the start button. If we are not running or in pause mode, we do not allow the creation of turrets so we simply return.

Secondly we need to know if we can afford to build the turret. currentCash is another global that stores how much money the player has at the moment and we check it against the turretValue function. We use the turret passed in through the outer function to do this.

The bulk of the code in the middle here is getting the mouse coordinates, so we know where to draw our new turret object. Finally we create a turretD of class turretdrag and set the draggable property to true. We then add our turretDrag event handler to this object (discussed below) and finally reduce our total cash by the cost to construct this turret.

Drag and Drop

As we saw above, turretdrag objects are draggable, and map elements that aren't part of the path are able to have items dropped on them. Now we will look at the event handlers for these.

Firstly, turretDrag:

  1.  
  2. function turretDrag(turret)
  3. {
  4. function drag(evt)
  5. {
  6. evt = evt || window.event;
  7. evt.dataTransfer.effectAllowed = 'copy';
  8. evt.dataTransfer.setData("Text",turret.id);
  9. }
  10. return drag;
  11. }
  12.  

This function takes a turretdrag object and returns an event handler function. We set the drag type to be 'copy' (both the drag and drop type must be the same (options are copy, move or copyMove) or you will not be able to drop the object in place). This is set through the dataTransfer object which carries information along with the object. We set the text data property of the dataTransfer object to be the ID of the turret that is being dragged.

  1.  
  2. function dragOver(evt)
  3. {
  4. if(evt.preventDefault) evt.preventDefault();
  5. evt = evt || window.event;
  6. evt.dataTransfer.dropEffect = 'copy';
  7. return false;
  8. }
  9.  

This is the event handler for dragOver, which is triggered when a draggable object passes over a map object. Here we set the drop effect to be 'copy' to match the drag object, and we return false to stop the event propagating to any higher elements, we want this map tile in particular to handle the event. The preventDefault call removes the default event handler.

Now the last piece of the puzzle is the code to handle dropping the turret in place:

  1.  
  2. function mapDrop(mapzone)
  3. {
  4. function drop(evt)
  5. {
  6. cancelPropogation(evt);
  7. evt = evt || window.event;
  8. evt.dataTransfer.dropEffect = 'copy';
  9. var id = evt.dataTransfer.getData("Text");
  10. var turret = document.getElementById(id);
  11. turret.style.left = mapzone.style.left;
  12. turret.style.top = mapzone.style.top;
  13.  
  14. // get the drop coordinates
  15. var x = mapzone.style.left.replace(/\D/g,"");
  16. var y = mapzone.style.top.replace(/\D/g,"");
  17.  
  18. // the id is up to the colon in the string
  19. var turretID = turret.id.substring(0,turret.id.indexOf(":"));
  20.  
  21. // store an entry in the turret position array
  22. turretPos[numTurrets++] = new Array(turretRange(turretID),turretDamage(turretID),x,y);
  23.  
  24. // once its droppable, you can't move it anymore
  25. turret.setAttribute("draggable","false");
  26. listenEvent(turret,"dragstart",cancelEvent);
  27. }
  28. return drop;
  29. }
  30.  

We read the ID out of the text element we set up earlier in the dataTransfer object. This gives is the ID of the element representing the dragged turret. We set the turret coordinates to match those of the tile it has been dropped on to.

We want to store the turret position into a global array so it can be accessed later without having to query the element directly again, which can be a costly operation. We define the following globals:

  1.  
  2. var turretPos = new Array();
  3. var numTurrets = 0;
  4.  

turretPos is a two dimensional array. Each line of the array consists of an array with four entries: the range of the turret, the damage the turret inflicts per fire and the x and y coordinates of the turret.

The regular expression used in the next statement matches everything that isn't a digit, and replaces it with an empty string. The reason for this is that if the style.left property will be rendered as 100px, not 100, so we need to remove the px from it before we can store it as a number for calculation later.

To call our helper functions we need the original turret type ID, not the ID of the turretdrag object. However I constructed the IDs to be read as "turretx:##" where turretx is an ID of one of the 5 generating turrets. We strip that out to use in our helper functions turretRange and turretDamage, reproduced below:

  1.  
  2. function turretRange(turretID)
  3. {
  4. switch(turretID)
  5. {
  6. case "turret0":
  7. return 3*TILE_W;
  8. case "turret1":
  9. return 5*TILE_W;
  10. case "turret2":
  11. return 10*TILE_W;
  12. case "turret3":
  13. return 15*TILE_W;
  14. case "turret4":
  15. return 20*TILE_W;
  16. }
  17. }
  18.  
  19. function turretDamage(turretID)
  20. {
  21. switch(turretID)
  22. {
  23. case "turret0":
  24. return 1;
  25. case "turret1":
  26. return 3;
  27. case "turret2":
  28. return 5;
  29. case "turret3":
  30. return 10;
  31. case "turret4":
  32. return 20;
  33. }
  34. }
  35.  

Once again these values are arbitary and can be changed depending on what kind of play type you want. Keep in mine that turrets fire 100 times a second when setting the damage.

Once a turret is set we don't want to be able to move it again. So we set the draggable property to false and cancel the dragstart event.

Lets Play!

Here is the event handler that gets called when start is pressed:

  1.  
  2. function startwave(evt)
  3. {
  4. if(isRunning) return;
  5. isRunning = true;
  6.  
  7. // make the pause button visible
  8. var sb = document.getElementById("startbutton");
  9. sb.innerHTML = "<p> Pause </p>";
  10. listenEvent(sb,"click",pausewave);
  11. // reset globals
  12. currentWave = 0;
  13. currentLives = 10;
  14. currentCash = 20;
  15. currentScore = 0;
  16. turretPos.length = 0;
  17. numTurrets = 0;
  18.  
  19. // increase the wave count
  20. currentWave++;
  21.  

First note we change the text of the start button to say "Pause" and change the click event handler to be pausewave, whcih is the following:

  1.  
  2. function pausewave(evt)
  3. {
  4. isPaused = !isPaused;
  5. }
  6.  

Which toggles pause mode when playing. We then set up the global variables which control the player state, and reset any placed turrets by clearning the turretPos array. We increment our wave counter to 1, the first wave of minions is about to be relased!

  1.  
  2. // remove all the placed turrets
  3. var turrets = document.querySelectorAll(".turretdrag");
  4. for(var i = 0; i < turrets.length; i++)
  5. {
  6. document.body.removeChild(turrets[i]);
  7. }
  8.  
  9. // create all our minions
  10. for(var i = 0; i < minion_count; i++)
  11. {
  12. var minion = document.createElement("div");
  13. minion.setAttribute("id","minion"+i);
  14. minion.setAttribute("class","minion");
  15. document.body.appendChild(minion);
  16. }
  17.  
  18.  

Here we remove any placed turrets and place the minions. The minions are black circles and are defined by this style sheet:

  1.  
  2. .minion
  3. {
  4. background-color: #000000;
  5. position: absolute;
  6. width: 15px; height: 15px;
  7. -moz-border-radius: 8px;
  8. -webkit-border-radius: 8px;
  9. top: 0px; left: 0px;
  10. }
  11.  
  1.  
  2. // set up the timers to run
  3. var movex = new Array();
  4. var movey = new Array();
  5. // what direction are we going?
  6. var currentDir = new Array();
  7. var minion_c = 1;
  8. var minion_release = new Array();
  9. var minion_hp = new Array();
  10. var first_kill = new Array();
  11. var minions_killed = 0;
  12. var lives_lost = 0;
  13. var wave_over = false;
  14. // get all the minions available
  15. var minions = document.getElementsByClassName("minion");
  16. for(var i = 0; i < minions.length; i++)
  17. {
  18. movex[i] = 0;
  19. movey[i] = 0;
  20. currentDir[i] = MOVE_S;
  21. minion_release[i] = 0;
  22. minions[i].style.display = "none";
  23. minion_hp[i] = minionhp();
  24. first_kill[i] = true;
  25. }
  26.  

We set up any local variables required. There are quite a lot here, so I will discuss each and its purpose:

  • movex is an array holding the current x position of every minion
  • movey is an array holding the current y position of every minion
  • currentDir is an array holding the current direction of every minion. Direction can be one of the following:
      1.  
      2. const MOVE_N = 1;
      3. const MOVE_S = -1;
      4. const MOVE_E = 2;
      5. const MOVE_W = -2;
      6. const MOVE_END = -99;
      7.  

      The directions correspond to compass directions with END indicating the minion has left the map

  • minion_c represents how many minions have been released. The minions are released in a staggered fashion
  • minion_hp is an array representing the current hit points of every minion
  • first_kill is an array indicating that the minion has been killed
  • minions_killed keeps track of how many minions are killed
  • lives_lost indicates how many minions escape the map without being destroyed
  • wave_over indicates the wave is over, either all minions are killed or all lives are lost

We then get all the minions and set up the arrays to initial values.

minionhp() is a simple function to calculate the hit points of a minion. Currently it is set as:

  1.  
  2. function minionhp()
  3. {
  4. return Math.pow(2,currentWave)*100;
  5. }
  6.  

The minion hp will be 2^currentWave * 100, so for wave 1 it will be 200 per minion, wave 2 400 per minion etc. This value is fairly arbitary and is subject to tweaking based on gameplay.

  1.  
  2. interval_id = setInterval(function()
  3. {
  4. if(!isPaused)
  5. {
  6. for(var i = 0; i < minion_c; i++)
  7. {
  8. // what direction do we want to go?
  9. currentDir[i] = whereToMove(movex[i],movey[i],currentDir[i]);
  10.  
  11.  

We use setInterval to set up a timer, which ticks every 10ms. The timer has an inner function that gets executed every 10ms, and this function does all the work of actually playing the game. We check if the game is paused or not, and if it is not we loop up to the number of minions that have currently been released.

The first step is to check where the minion should move. We call the function whereToMove:

  1.  
  2. function whereToMove(xpos, ypos, currentDir)
  3. {
  4. // convert the xpos and ypos to block coordinates
  5. xpos = (xpos + TILE_W/2) / TILE_W;
  6. ypos = (ypos + TILE_H/2) / TILE_H;
  7.  
  8. var xnewpos = Math.floor(xpos);
  9. var ynewpos = Math.floor(ypos);
  10.  
  11. // test out some possible move locations
  12. switch(currentDir)
  13. {
  14. case MOVE_N:
  15. ynewpos -= 1;
  16. break;
  17. case MOVE_S:
  18. ynewpos += 1;
  19. break;
  20. case MOVE_E:
  21. xnewpos += 1;
  22. break;
  23. case MOVE_W:
  24. xnewpos -= 1;
  25. break;
  26. }
  27.  
  28.  
  29. // are we still on the map?
  30. if(level1(Math.floor(xnewpos),Math.floor(ynewpos)))
  31. {
  32. // ok! keep going in the same direction
  33. return currentDir;
  34. }
  35.  
  36. // we have fallen off the map! Find out where to go...
  37. if(level1(Math.floor(xpos) + 1,Math.floor(ypos)) && currentDir != -MOVE_E)
  38. {
  39. return MOVE_E;
  40. }
  41. if(level1(Math.floor(xpos) - 1, Math.floor(ypos))&& currentDir != -MOVE_W)
  42. {
  43. return MOVE_W;
  44. }
  45. if(level1(Math.floor(xpos),Math.floor(ypos) + 1)&& currentDir != -MOVE_S)
  46. {
  47. return MOVE_S;
  48. }
  49. if(level1(Math.floor(xpos),Math.floor(ypos) - 1)&& currentDir != -MOVE_N)
  50. {
  51. return MOVE_N;
  52. }
  53.  
  54. // if all fails, we have reached the end of the map
  55. return MOVE_END;
  56. }
  57.  

We convert our xpos and ypos from pixels to coordinates in terms of tiles. Then we make sure the next tile in the current direction is a path tile. If it is not, we look in each compass direction to see which way we should turn.

We also have to make sure we don't go back the way we came, so each direction is set up so its negative value is the opposite direction, and we check to make sure this condition is not true. If we have no where to go we have reached the end of the map, and we return MOVE_END.

  1.  
  2. if(currentDir[i] == MOVE_END)
  3. {
  4. // lose a life, one escaped!
  5. if(minions[i].style.display != "none")
  6. {
  7. currentLives--;
  8. lives_lost++;
  9. }
  10. // we have reached the end of the map
  11. minions[i].style.display = "none";
  12. if(currentLives == 0)
  13. {
  14. // game over
  15. wave_over = true;
  16. break;
  17. }
  18. // do we have minions killed?
  19. if(minions_killed == (minions.length - lives_lost))
  20. {
  21. // wave over!
  22. wave_over = true;
  23. }
  24. continue;
  25. }
  26.  
  27.  

This means we lost a life! We reduce the number of lives we have. If we get to zero lives its game over. We set the style of the minion to "none" to remove it from the screen so it disappears as it goes off the edge. We make sure we only decrement the currentLives value once, as the minion still exists (it is just invisible) this code will get executed every tmie this part of the array is encountered, ie every 10ms still! We have another check - if we kill all the minions the still continue to move, however all will move off the edge. We have to make sure we terminate normally if some but not all are killed.

  1.  
  2. switch(currentDir[i])
  3. {
  4. case MOVE_N:
  5. movey[i] -= 1;
  6. break;
  7. case MOVE_S:
  8. movey[i] += 1;
  9. break;
  10. case MOVE_E:
  11. movex[i] += 1;
  12. break;
  13. case MOVE_W:
  14. movex[i] -= 1;
  15. break;
  16. }
  17.  

If we get a direction to move in that isn't the end, move 1 pixel in that direction. Remember this function is called 100 times a second, so the minions will move at a rate of 100px a second. Changing this value here can change the speed of the minions.

  1.  
  2. minions[i].style.display = "block";
  3. minions[i].style.top = movey[i] + "px";
  4. minions[i].style.left = movex[i] + "px";
  5.  
  6. // are there any turrets in range?
  7. var damage = anyTurretsInRange(minions[i],movex[i],movey[i]);
  8.  

We move the minion, and make it visible. We started with the minion invisible so it appears to be moving from off screen when it first moves.

Here after moving we check if there are any turrets within firing range:

  1.  
  2. function anyTurretsInRange(minion,x,y)
  3. {
  4. var score = document.getElementById("score");
  5. var damage = 0;
  6. for(var i = 0; i < numTurrets; i++)
  7. {
  8. // get the x and y positions of the turret
  9. var xt = turretPos[i][2];
  10. var yt = turretPos[i][3];
  11.  
  12. if(euclidDistance(x,xt,y,yt) <= turretPos[i][0])
  13. {
  14. minion.style.backgroundColor = "#FF0000";
  15. damage += turretPos[i][1]; // return the damage
  16. }
  17. }
  18. if(damage == 0)
  19. {
  20. // nothing in range
  21. minion.style.backgroundColor = "#000000";
  22. }
  23. return damage;
  24. }
  25.  

We use our turretPos array from earlier and get the position of every turret on the field. We then calculate the Euclidean distance between the two points, as shown:

  1.  
  2. function euclidDistance(x1,x2,y1,y2)
  3. {
  4. return Math.sqrt(Math.pow(x1-x2,2) + Math.pow(y1-y2,2));
  5. }
  6.  

If this is less than the range of the turret (stored in turretPos[i][0]) we have a hit! Turn the minion red to indicate a hit and calculate the damage from whatever the value is in turretPos[i][1] which stored the damage of that turret.

Finally if there is no damage had turn the minion back to black again.

  1.  
  2. // reduce the minion's hit points by the damage
  3. minion_hp[i] -= damage;
  4. if(minion_hp[i] <= 0)
  5. {
  6. // goodbye minion!
  7. if(first_kill[i])
  8. {
  9. first_kill[i] = false;
  10. minions_killed++;
  11. // increase your cash a little bit
  12. currentCash += minionreward();
  13. currentScore++;
  14. if(minions_killed == (minions.length - lives_lost))
  15. {
  16. // wave over!
  17. wave_over = true;
  18. }
  19. }
  20. minions[i].style.display = "none";
  21. }
  22.  

We reduce the hitpoints of the minion by the damage taken. If this takes the minion's hp to zero or less its dead! Turn its display to "none" to hide it from the display. We reward the player with some cash, calculated as:

  1.  
  2. function minionreward()
  3. {
  4. return Math.pow(currentWave+1,2);
  5. }
  6.  

This means the reward is the current wave + 1 squared in dollars. This again was arbitary and subject to change for gameplay reasons. As the game is essentially infinite, tweaking this value can cause people not be able to build fast enough and hence die at some point...

If all the minions are killed the wave is over!

  1.  
  2. // stagger the minions coming out, release one every 15 pixels
  3. if((minion_release[i] == 100*minion_c) && minion_c < minions.length)
  4. {
  5. minion_c++;
  6. }
  7. minion_release[i]++;
  8.  

Finally we don't want the minions coming out all at once. I have set this so that only ever 100 ticks (once a second) a minion will appear. Again this is changeable depending on what type of game you want. Just make sure the minions don't come out so fast they overlap, ie slower than 15 seconds per minion. This staggering only continues until we have all the minions out.

  1.  
  2. }
  3. // update the status
  4. updateStatus();
  5.  
  6.  

We want to update the player state:

  1.  
  2. function updateStatus()
  3. {
  4. // update all the status variables
  5. var cash = document.getElementById("cash");
  6. cash.innerHTML = "$" + currentCash;
  7.  
  8. var score = document.getElementById("score");
  9. score.innerHTML = currentScore;
  10.  
  11. var wave = document.getElementById("wave");
  12. wave.innerHTML = currentWave;
  13.  
  14. var lives = document.getElementById("lives");
  15. lives.innerHTML = currentLives;
  16.  
  17. // highlight turrets we can purchase
  18. var turrets = document.getElementsByClassName("turret");
  19. for(var i = 0; i < turrets.length; i++)
  20. {
  21. if(currentCash >= turretValue(turrets[i].id))
  22. {
  23. turrets[i].style.opacity = 1;
  24. }
  25. else
  26. {
  27. turrets[i].style.opacity = 0.5;
  28. }
  29. }
  30. }
  31.  

The globals are written out to the status panel. Also if we can afford a turret, it now activates by turning full solid. If we can't afford a turret, its opacity is reduced back to 0.5.

  1.  
  2. // is the wave over?
  3. if(wave_over)
  4. {
  5. if(currentLives == 0)
  6. {
  7. var lives = document.getElementById("lives");
  8. lives.innerHTML = "Game Over";
  9. resetwave(null);
  10. }
  11. // reset for the next wave!
  12. minion_c = 1;
  13. minions_killed = 0;
  14. wave_over = false;
  15. currentWave++;
  16. for(var i = 0; i < minions.length; i++)
  17. {
  18. movex[i] = 0;
  19. movey[i] = 0;
  20. currentDir[i] = MOVE_S;
  21. minion_release[i] = 0;
  22. minions[i].style.display = "none";
  23. minion_hp[i] = minionhp();
  24. first_kill[i] = true;
  25. }
  26. }
  27. }
  28. },10);
  29. }
  30.  

If the wave is over we reset all the variables and increase the currentWave variable. If we have no lives left, we output a Game Over message and call resetwave, which is also the click handler for the reset button.

Remember this was an inner function, so that final 10 is the second parameter to setInterval, setting this function to execute every 10ms.

  1.  
  2. function resetwave(evt)
  3. {
  4. if(!isRunning) return;
  5. isRunning = false;
  6.  
  7. // make the start button visible
  8. var sb = document.getElementById("startbutton");
  9. sb.innerHTML = "<p> Start! </p>";
  10. listenEvent(sb,"click",startwave);
  11.  
  12. // stop the timers
  13. clearInterval(interval_id);
  14.  
  15. // remove all the minions
  16. var minions = document.querySelectorAll(".minion");
  17. for(var i = 0; i < minions.length; i++)
  18. {
  19. document.body.removeChild(minions[i]);
  20. }
  21.  
  22. }
  23.  

This resets the global variable and removes any minions present. The towers will be removed when start is pressed again.

Loading the script

The entry point of the application is as follows:

  1.  
  2. window.onload=function()
  3. {
  4. drawMap();
  5. }
  6.  

When the window loads the drawMap function is called and starts everything off.

Any questions feel free to ask. Looking for more playtesters, so any feedback on the game is helpful too.

This entry was posted in css, html, javascript, programming. Bookmark the permalink.

One Response to A Simple Tower Defense Game in Javascript

  1. Pingback: A Brainfuck Interpreter in Javascript | Coding Arterial

Leave a Reply