Roguelike, step 4: attack!

Previous tutorial – Download files – Next tutorial
  

A hero in the flesh – Monsters, three ways – When heroes attack, dice will roll – Dealing with the corpse – self.dot, self:colon 

A hero in the flesh

We promised our hero a sword, so let’s give them a sword.

self.hero = Player.new()

Of course, this only actually hands them a sword if there’s a class called Player with a Player:init() function that contains a self.weapon variable that contains a sword.  By now, we’re familiar with the process of throwing swords and anything else we need into a class init function so let’s create a Player class in a new Player.lua file.

Player = Core.class() function Player:init()  

  --position in the manual and tileset-monsters 
  self.entry = 1 self.x, self.y = 6, 6 
  self.light = manual:getEntry("light-source", "torch")  

  --equip the hero with a longsword for now 
  self.weapons = {{name = "longsword", reach = 1, modifier = 2, 
                  defense = "AC", damage = 'd8', dice = 1, bonus = 2, type = nil, 
                  projectile = nil, missSound = "clang"}, 
                  {name = "shortbow", reach = 5, modifier = 2, 
                  defense = "AC", damage = 'd6', dice = 1, bonus = 2, type = nil,
                  projectile = "arrow", missSound = "swish"},} 
  self.weapon = self.weapons[1]  

  --defense variables 
  self.xp = 0 
  self.hp = 20 
  self.maxHP = self.hp 
  self.bloodied = 10 
  self.defense = {AC = 16, Fort = 16, Refl = 14, Will = 12}  
  --resistances below. vulnerabilities would be listed as negative (-5, etc..) 
  self.resist = {"all 0", "green 0", "red 0", "blue 0", "white 0", "black 0"} 
end

Too much?  Actually, anything is possible at this point.  There are many RPG rulesets out there to choose from or just roll your own.  On attack, the basic idea is a weapon or spell variable that contains everything we want a an attack to do.  Defensively, the basics include HP and defenses such as AC.  Many things are possible here.  Race and class.  Skills and conditions.  Just keep adding until you’ve captured the flavor of your favorite system.  Or keep it simple for now and you can always add more later.

For my system, our attackMonster function will use the weapon variable stats to roll for the attack’s success and determine the damage amount.  When attacking, weapon.modifier will be added to the attack roll and compared to the monster.AC.  The longsword will roll 1d8+2 to determine how much untyped damage is done.  It is not a projectile and “clang’ will be communicated to the player when an attack misses.

Monsters, three ways 

For the hero, I typed everything in the Player class.  Alternatively, I could have added everything to the manual in Constants.lua and then pulled it out with

 local info = manual:getEntry("monsters", hero.entry)

This is the approach we’ll take for the Monster class.  First, let’s add everything to the manual that we want to define each monster as.

 ["monsters"] = {
   [1] = {name = "hero"},
   [2] = {name = "goblin", xp = 1, hp = 4, bloodied = 2, 
          defense = {AC = 16, Fort = 12, Refl = 14, Will = 11}, 
          resist = {"all 0", "green 0", "red 0", "blue 0", "white 0", "black 0"}, 
          weapons = {
            {name = "knife", 
               reach = 1, modifier = 2, defense = "AC", damage = 'd4', dice = 1, 
               bonus = 3, type = nil, projectile = nil, missSound = "clang"},}},
   [3] = {name = "goblin warrior", xp = 3, hp = 25, bloodied = 12, 
          defense = {AC = 17, Fort = 13, Refl = 15, Will = 12}, 
          resist = {"all 0", "green 0", "red 0", "blue 0", "white 0", "black 0"}, 
          weapons = {
            {name = "shortsword", 
               reach = 1, modifier = 4, defense = "AC", damage = 'd6', dice = 1, 
               bonus = 2, type = nil, projectile = nil, missSound = "clang"},
            {name = "javelin", 
               reach = 5, modifier = 6, defense = "AC", damage = 'd8', dice = 1, 
               bonus = 2, type = nil, projectile = "spear", missSound = "swish"},}},
   [4] = {name = "bugbear", xp = 4, hp = 35, bloodied = 17, 
          defense = {AC = 19, Fort = 17, Refl = 15, Will = 13}, resist = {"all 0", "green 0", "red 0", "blue 0", "white 0", "black 0"}, 
          weapons = {
            {name = "battleaxe", 
               reach = 1, modifier = 6, defense = "AC", damage = 'd10', dice = 1, 
               bonus = 5, type = nil, projectile = nil, missSound = "clang"},
            {name = "hacking frenzy", 
               reach = 1, modifier = 8, defense = "AC", damage = 6, dice = 1, 
               bonus = 2, type = nil, projectile = "spear", missSound = "swish"},},},},

We now have three, fairly well-defined monsters.  With this information, we can create a Monster class in a new Monsters.lua file.

Monster = Core.class()

function Monster:init(entry)

  --position in the manual and tileset-monsters
  self.entry = entry 
  -- use 1, 1 as temporary a location
  self.x, self.y = 1, 1

  --info is the entry in the monster manual 
  local info = manual:getEntry("monsters", entry)

  --their defense variables
  self.hp = info.hp
  self.maxHP = self.hp
  self.defense = info.defense
  self.resist = info.resist
 
  --their attack variables
  self.weapons = info.weapons
  self.weapon = self.weapons[1]

end

Let’s really simplify the code in main.lua while we’re at it.  To create our game, we’ll call our three gaming variables.

 hero = Player.new()
 monsters = Monsters.new()
 world = WorldMap.new(hero, monsters)

You’ll notice I prefer to create all the monsters all at once, instead of individually calling Monster.new() six times.  To do so, create another class in Monsters.lua.  This one we’ll call Monsters which will call the individual Monster class for us.

Monsters = Core.class(Sprite)

function Monsters:init()

  self.list = {}
  --place three #2 monsters 
  for i = 1, 3 do
    table.insert(self.list, Monster.new(2))
  end
  --place two #3 monsters 
  for i = 1, 2 do
    table.insert(self.list, Monster.new(3))
  end
  --place one #4 monster
  table.insert(self.list, Monster.new(4))
end

After a whole lot of typing, our hero and monsters are ready to face off.  Let’s write a function to make it happen.

When heroes attack, dice will roll 

Over in checkMove(), we know the player wants to attack a monster when they maneuver the hero next to a monster tile and then bump into them.  So we’ll insert our attackMonster function there.

if layer == LAYER_MONSTERS then 
  attackMonster(hero.x + dx, hero.y + dy)

And then attackMonster will start like this.

function Game:attackMonster(x, y)

  --this is the hero's weapon
  local weapon = hero.weapon

  --find the monster being attacked and their index in monsters.list 
  local monster = nil 
  local id = 0 
  for i = 1, #monsters.list do
    monster = monsters.list[i] 
    id = i
    if monster.x == x and monster.y == y then 
      break 
    end
  end

We’re simply identifying the variables at play.  Local weapon is the weapon the hero has equipped.  Local monster goes through the monsters list until the one with the same coordinates as the targeted one is found.

The rest of the attackMonster() function is also straightforward.  Call a rollDice() function and compare the result to the monster.defense that the weapon is targeting.  Misses are reported slightly differently then close misses.

  --roll for the attack
  local roll = rollDice(weapon.modifier)
  if roll < monster.defense[weapon.defense] then
    --report misses and close misses
    if monster.defense[weapon.defense] - roll > 3 then
      print("miss")
    else 
      print(weapon.missSound)
    end
  else
    --roll for damage
    roll = rollDice(weapon.bonus, weapon.damage, weapon.dice)
    print(roll)
    monster.hp = monster.hp - roll
    if monster.hp < 1 then
      --remove the monster from the monsters.list and the map
      table.remove(monsters.list, id) 
      world:removeMonster(x, y) 
      print("you killed it")
    elseif monster.hp < 3 then
      print("it's almost dead")
    end
  end
end

A hit results in another rollDice() call to determine damage which is then applied to the monster’s hp.  Clearly, the rollDice() function is heart of what we’re doing here.

function rollDice(modifier, maximum, die, crit)

  local mod = modifier or 0
  local max = maximum or "d20"
  --remove the 'd' from d4, d20, etc..
  local max = tonumber(string.sub(max, 2))
  local dice = die or 1
  local critical = crit or max

  local result = 0
  for roll = 1, dice do
    result = result + math.random(1, max)
  end
 
  if result >= critical then
    return result + mod, true
  else
    return result + mod, false
  end
end

rollDice() returns a random result between 1 and 20 whether the result was a critical hit.  I wrote rollDice to take optional arguments.  If you want to roll two d8 dice, call rollDice(0, 8, 1).  If you just want a plain 1d20, use rollDice().  For whatever system you’re aiming for, determine rollDice’s default behavior by changing the four arguments  that are passed to it.  A passed modifier can be added to the 1d20 roll.  If no modifier is detected, the variable mod is assigned 0.

This works because the or operator goes through the expression one argument at a time.  If the first argument is false or nil, the second argument is returned.  

mod = modifier or 0 

is similar to if statements in other languages:

if modifier then mod = modifier else mod = 0

The rest of the arguments also have default values.  Maximum is the optional die to use:  d8, d10, d12, etc.., with d20 as the default.  Die is how many dice are used and crit is the critical hit minimum without the modifier (default is 20).

Dealing with the corpse

With rollDice() in place, our attackMonster() function will work until the monster dies.  Monster death means the monster is removed from monsters.list and from the WorldMap maparray and TileMap.  At which point WorldMap:removeMonster() is called.  That function, over is TileMaps.lua, is below.

function WorldMap:removeMonster(x, y)

  local mArray = self.mapArrays[LAYER_MONSTERS]
  mArray[x + (y - 1) * LAYER_COLUMNS] = 0
  self.mapLayers[LAYER_MONSTERS]:clearTile(x, y) 
end

self.dot, self:colon 

With attackMonster() in place, our hero is free to wander the dungeon, dispatching monsters as is his or her want.  We’re done for now if we want to be, but…..

I’m going to recommend one more thing.  Did you notice when we added the rollDice() function to Main.lua, that it had to be listed before the function that called it?  But when we added WorldMap:removeMonster(), we could put it anywhere in Tilemaps.lua?  That’s because in Main.lua rollDice is a local function, while in Tilemaps.lua, removeMonster is a class function.  For pretty much this reason only, I’m going to recommend we create a Game.lua file and transfer almost all of Main.lua over to it.  If we now transfer the rest of Main.lua into Gamelogic.lua and create a class to hold it, Game:init() will look like this.

Game = Core.class(Sprite)
 
function Game:init()

  --the major gaming variables
  hero = Player.new()
  monsters = Monsters.new()
  world = WorldMap.new(hero, monsters)

  --get everything on the screen
  addChild(world)
  main = MainScreen.new()
  addChild(main)
 
  --respond to the compass directions
  main.north:addEventListener("click", function() checkMove(0, -1) end)
  main.south:addEventListener("click", function() checkMove(0, 1) end)
  main.west:addEventListener("click", function() checkMove(-1, 0) end)
  main.east:addEventListener("click", function() checkMove(1, 0) end)
end

Except it won’t work!?  The reason it won’t work, is we need to turn all the variables into class variables.  hero = Player.new() into self.hero = Player.new().  monsters into self.monsters and world into self.world = WorldMap.new(self.hero, self.monsters).

function Game:init()

  --the major gaming variables
  self.hero = Player.new()
  self.monsters = Monsters.new()
  self.world = WorldMap.new(self.hero, self.monsters)

  --get everything on the screen
  self:addChild(self.world)
  self.main = MainScreen.new()
  self:addChild(self.main)
 
  --respond to the compass directions
  self.main.north:addEventListener("click", function() self:checkMove(0, -1) end)
  self.main.south:addEventListener("click", function() self:checkMove(0, 1) end)
  self.main.west:addEventListener("click", function() self:checkMove(-1, 0) end)
  self.main.east:addEventListener("click", function() self:checkMove(1, 0) end)
end

We also need to turn all the functions into class functions.  So function checkMove(dx, dy) into function Game:checkMove(dx, dy) and the same for attackMonster() and rollDice().  As consequence, when we call these functions, they’re called as class functions with self:checkMove as above when the compass EventListeners respond to the player.

Within our class functions, if we need to add a Sprite to the stage, we’ll need to do so with a self:addChild() call instead of the simple addChild() function call we made in Main.lua.  Therefore, when we call MainScreen.new() in Game:init() and want to add it to the stage, we’ll do so with

self:addChild(self.main)

If you’ve come from a programming language like Python, you recognize the logic behind all this self-ification.   If you’re a beginner programmer, now is a good time to wrap your head around this stuff because even though Lua doesn’t use classes, Gideros Mobile definitely does.  Even if you don’t grasp it completely, remember to begin all class variables with a self. and use self: to make all class function calls.  If you don’t, or if you use a ‘.’ when a ‘:’ is called for, strange error messages will appear in your code.

The final benefit of creating Gamelogic.lua is that Main.lua is now all of seven lines long.

--set up the application variables
application:setLogicalDimensions(1188, 1920)
application:setOrientation(Application.LANDSCAPE_LEFT)
application:setScaleMode("letterbox")
application:setBackgroundColor(COLOR_BLACK)

--set up the manual
manual = Cyclopedia.new()

--create the game
local game = Game.new()
stage:addChild(game)

Do you realize that our compact program is turning into a sprawling program?  Main.lua is now joined by seven other files, but hopefully its evolution has been easy to keep track of and visualize.  We’ll continue to add features to our game by organizing it in different classes spread across different files.  Doing so will allow us to focus on one feature at a time, without having to grok the whole thing all at once.

Summary

With this step, we gave our hero a sword and removed the corposes of the monsters they killed.  Along the way, we imagined communicating with the player what the hero was doing with statements such as

print("you killed it")

and

print("miss")

Unfortunately, these print commands are only seen on Gidero Mobile’s output section and are not visible to the player.

print

Let’s address how we communicate game information to the player in the next Step.

Next up:   health talk.