This is the seventh post in the Building a Roguelike in Javascript series. I recommend you start at the beginning unless you've been following along. This part corresponds to the fifth part in Trystan's series. All the code for this part can be found at the part 5b tag of the jsrogue repository. At the time of writing, I am still using the d4ea2ab commit for rot.js.

Today we're going to make it so that we can interact with our fungi! We're going to add a basic attack system which will simply remove an entity when our player attacks it. For those unfamiliar with roguelikes, attacking is generally done by bumping into or moving into the entity we wish to attack. We're also going to make the fungus entity spread randomly over time, eventually taking over our cave unless our hero chooses to rise to the task and attack every fungus!

Demo Link

The results after this post can be seen here

assets/map.js

Before we can get started with our attack system, we need a way to actually remove an entity from the map. One special consideration to make is that we must also remove the entity from the scheduler if they were an actor. So you're going to want to add the following function:

1 2 3 4 5 6 7 8 9 10 11 12 13 Game . Map . prototype . removeEntity = function ( entity ) { // Find the entity in the list of entities if it is present for ( var i = 0 ; i < this . _entities . length ; i ++ ) { if ( this . _entities [ i ] == entity ) { this . _entities . splice ( i , 1 ); break ; } } // If the entity is an actor, remove them from the scheduler if ( entity . hasMixin ( 'Actor' )) { this . _scheduler . remove ( entity ); } }

When we implement the fungi spreading, we're going to make it spread to an adjacent tile. In order to do this, we'll need to check whether a tile is an empty floor tile. As this will be a pretty common thing to check for, let's create a helper function which will do just that so we have a central place to add in features:

1 2 3 4 5 Game . Map . prototype . isEmptyFloor = function ( x , y ) { // Check if the tile is floor and also has no entity return this . getTile ( x , y ) == Game . Tile . floorTile && ! this . getEntityAt ( x , y ); }

While we're here, let's go ahead and update our getRandomFloorPosition to make use of this function:

1 2 3 4 5 6 7 8 9 Game . Map . prototype . getRandomFloorPosition = function () { // Randomly generate a tile which is a floor var x , y ; do { x = Math . floor ( Math . random () * this . _width ); y = Math . floor ( Math . random () * this . _width ); } while ( ! this . isEmptyFloor ( x , y )); return { x : x , y : y }; }

The last change we're going to want to make is to decrease the number of initial fungi. Because they will be spreading throughout the dungeon it will be much more apparent if we start with a small number. Feel free to play around with the number! We have to change:

1 2 3 4 5 6 7 Game . Map = function ( tiles , player ) { // ... // add random fungi for ( var i = 0 ; i < 50 ; i ++ ) { this . addEntityAtRandomPosition ( new Game . Entity ( Game . FungusTemplate )); } }

We're now ready to make it so we can hack away at these pesky growing fungi!

assets/entities.js

In order to implement our basic attacking system, we're going to have two mixins. The first wil be a mixin denoting an entity that is destructible. For now the most basic mixin will have a certain number of hit points and a takeDamage function. The takeDamage function will subtract the damage caused by an attacker, and if the hit points are 0 or below, remove that entity from the map. For now all Destructible entities will have 1 hit point.

1 2 3 4 5 6 7 8 9 10 11 12 13 Game . Mixins . Destructible = { name : 'Destructible' , init : function () { this . _hp = 1 ; }, takeDamage : function ( attacker , damage ) { this . _hp -= damage ; // If have 0 or less HP, then remove ourseles from the map if ( this . _hp <= 0 ) { this . getMap (). removeEntity ( this ); } } }

The second will be some kind of Attacker mixin, which has a sole method attack. When an entity chooses to attack a target, we will call the attack method with the target. Similar to the Actor mixin we implemented last post, the Attacker mixin will also be a common group (using the groupName) and we'll create more specific ones when necessary, allowing us to come up with really cool ways of attacking. To keep it simple, if our target is destructible, we will damage the target by 1 HP, removing it from the map.

1 2 3 4 5 6 7 8 9 10 Game . Mixins . SimpleAttacker = { name : 'SimpleAttacker' , groupName : 'Attacker' , attack : function ( target ) { // Only remove the entity if they were attackable if ( target . hasMixin ( 'Destructible' )) { target . takeDamage ( this , 1 ); } } }

Now we need to update our templates to include these mixins! We want to add a SimpleAttacker and Destructible to the player's template as in the future the player will be attackable. We also want to add Destructible to the fungi.

1 2 3 4 5 6 7 8 9 10 11 12 13 // Player template Game . PlayerTemplate = { character : '@' , foreground : 'white' , mixins : [ Game . Mixins . Moveable , Game . Mixins . PlayerActor , Game . Mixins . SimpleAttacker , Game . Mixins . Destructible ] } // Fungus template Game . FungusTemplate = { character : 'F' , foreground : 'green' , mixins : [ Game . Mixins . FungusActor , Game . Mixins . Destructible ] }

So now we have our attacking mixins set up and put in place. However we have to change our Moveable as well in order to make it so that if there is an entity present at the cell we wish to move to, and we are an attacker, than try to attack the entity!

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // Define our Moveable mixin Game . Mixins . Moveable = { name : 'Moveable' , tryMove : function ( x , y , map ) { var tile = map . getTile ( x , y ); var target = map . getEntityAt ( x , y ); // If an entity was present at the tile if ( target ) { // If we are an attacker, try to attack // the target if ( this . hasMixin ( 'Attacker' )) { this . attack ( target ); return true ; } else { // If not nothing we can do, but we can't // move to the tile return false ; } // Check if we can walk on the tile // and if so simply walk onto it } else if ( tile . isWalkable ()) { // Update the entity's position this . _x = x ; this . _y = y ; return true ; // Check if the tile is diggable, and // if so try to dig it } else if ( tile . isDiggable ()) { map . dig ( x , y ); return true ; } return false ; } }

Our attacking system is now put in place! At this point you can go ahead and try it out! If everything went well you should be able to go up to a fungus on the map, walk into it and it will dissapear! In a few posts we'll turn this into a real attacking system with stats and health and all sorts of other goodies, but for it's pretty pleasing to see what we've already accomplished!

The next step is to implement the fungus growth. Recall last post that the scheduling system worked by calling the act method of entities turn by turn. We had created an actor mixin for the fungus, but it didn't do anything yet. As this is called every time the fungus has a turn, this seems like it could be the perfect place to put our spreading logic!

We want each fungus to be able to spread in one of the eight adjacent squares, however to make it interesting we don't want to do it every turn. Instead we are going to make it so that every turn the fungus has a fixed chance of growing. We're also going to want to limit the number of times a fungus can grow to let's say 5. Remember that mixins have an optional init function which can be called when the mixin is created to add state to the fungus. For now let's make it set up our state with a counter keeping trakc of how many times we can grow.

1 2 3 4 5 6 7 8 9 Game . Mixins . FungusActor = { name : 'FungusActor' , groupName : 'Actor' , init : function () { this . _growthsRemaining = 5 ; }, act : function () { } }

At every turn, if a fungus can still grow we use Math.random, which returns a number between 0.0 and 1.0, to determine if it should grow this turn. If so, then we generate the coordinates of a random adjacent square and check if we can grow to it. If we can, we will spawn a new fungus at that position and update our counters!

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 Game . Mixins . FungusActor = { name : 'FungusActor' , groupName : 'Actor' , init : function () { this . _growthsRemaining = 5 ; }, act : function () { // Check if we are going to try growing this turn if ( this . _growthsRemaining > 0 ) { if ( Math . random () <= 0.02 ) { // Generate the coordinates of a random adjacent square by // generating an offset between [-1, 0, 1] for both the x and // y directions. To do this, we generate a number from 0-2 and then // subtract 1. var xOffset = Math . floor ( Math . random () * 3 ) - 1 ; var yOffset = Math . floor ( Math . random () * 3 ) - 1 ; // Make sure we aren't trying to spawn on the same tile as us if ( xOffset != 0 || yOffset != 0 ) { // Check if we can actually spawn at that location, and if so // then we grow! if ( this . getMap (). isEmptyFloor ( this . getX () + xOffset , this . getY () + yOffset )) { var entity = new Game . Entity ( Game . FungusTemplate ); entity . setX ( this . getX () + xOffset ); entity . setY ( this . getY () + yOffset ); this . getMap (). addEntity ( entity ); this . _growthsRemaining -- ; } } } } } }

assets/screens.js

We are now just about ready to watch our fungus growth! We're going to reduce the map size as the fungus growing rapidly gets out of hand and depending on your computer may make movement very sluggish. So I've shrunk the map down to 100 by 48:

1 2 3 4 5 6 7 8 9 10 11 Game . Screen . playScreen = { // ... enter : function () { var map = []; // Create a map based on our size parameters var mapWidth = 100 ; var mapHeight = 48 ; // ... } // ... }

Conclusion

We've now got a cave which gets overrun by fungus which our hero can then go and hack away with our simple attacking system! In the next post we are going to start focusing on real combat as well as giving some feedbak to the player through messages.

I hope you enjoyed this post and that you'll stick around for the next part! Remember that all the code for this part can be found at the part 5b tag of the jsrogue repository. Also please feel free to post any comments whether questions, clarifications or criticism!

Thanks for reading,

Dominic

Next Part

Part 6 - Combat and Messages