What we have now is pretty much a playable game already. The next two parts will take it a big step forward by focusing on progression. We’ll start with dungeon levels, then character progression and finally on updating the monster and item placement.

Second floor please

A staple of roguelikes is the stairs, which the player must find to advance to the next dungeon level. We will start by placing them, when generating a level. Right at the end of make_map:

// create stairs at the center of the last room
let (last_room_x, last_room_y) = rooms[rooms.len() - 1].center();
let mut stairs = Object::new(last_room_x, last_room_y, '<', "stairs", WHITE, false);
stairs.always_visible = true;
objects.push(stairs);

As you can see, it’s just a regular object! We must now let the player go down the stairs when standing on them and presses the '<' key. It’s easy to add this check at the end of handle_keys:

(Key { code: Text, .. }, "<", true) => {
    // go down stairs, if the player is on them
    let player_on_stairs = objects
        .iter()
        .any(|object| object.pos() == objects[PLAYER].pos() && object.name == "stairs");
    if player_on_stairs {
        next_level(tcod, game, objects);
    }
    DidntTakeTurn
}

The most important bit is the next_level function. We need to generate a brand new level when the player goes down and move player onto it. We’ll also heal the player because we’re feeling generous!

/// Advance to the next level
fn next_level(tcod: &mut Tcod, game: &mut Game, objects: &mut Vec<Object>) {
    game.messages.add(
        "You take a moment to rest, and recover your strength.",
        VIOLET,
    );
    let heal_hp = objects[PLAYER].fighter.map_or(0, |f| f.max_hp / 2);
    objects[PLAYER].heal(heal_hp);

    game.messages.add(
        "After a rare moment of peace, you descend deeper into \
         the heart of the dungeon...",
        RED,
    );
    game.dungeon_level += 1;
    game.map = make_map(objects);
    initialise_fov(tcod, &game.map);
}

You can try it out now. We do generate stairs and let the player go deeper, but you’ll notice that the items and monsters from the old level are still here — sometimes lodged in a wall.

The reason we’re seeing old objects is because we’ve never removed them from the objects Vec!

So we need to remove everything except for the Player. Put this in make_map right after we create the empty map:

// Player is the first element, remove everything else.
// NOTE: works only when the player is the first object!
assert_eq!(&objects[PLAYER] as *const _, &objects[0] as *const _);
objects.truncate(1);

Luckily, Vec has the truncate method, which leaves the first n elements and removes everything else. Since our player is always the first object in the list, this just works.

The assert is there to make sure that the first object is indeed the player — the program will panic otherwise, making sure you address it if you change your game layout at some point. By the way, this is how you test for a pointer equivalence in Rust: you convert ordinary Rust references to raw pointers (*const Object in this case) and compare those. This is safe because we never dereference them. As a bonus, the compiler can infer the type (Object) so you can just say _ and it will fill it in.

If you want to let the player go back up, you may want to keep track of the map and objects at the previous levels so that they stay the same.

We’ll want to keep track of the dungeon level the player is on. Let’s add a variable to Game and set it to one when we start the game and increase it when we go deeper:

struct Game {
    map: Map,
    messages: Messages,
    inventory: Vec<Object>,
    dungeon_level: u32,  (1)
}
1 New field: dungeon_level

in new_game:

let mut game = Game {
    // generate map (at this point it's not drawn to the screen)
    map: make_map(&mut objects),
    messages: Messages::new(),
    inventory: vec![],
    dungeon_level: 1,  (1)
};
1 Set dungeon_level to 1

and in next_level before calling make_map:

game.dungeon_level += 1;

Now we can display it in the GUI. Add this line to render_all after calling render_bar:

tcod.panel.print_ex(
    1,
    3,
    BackgroundFlag::None,
    TextAlignment::Left,
    format!("Dungeon level: {}", game.dungeon_level),
);

Finally, it would be great to always show the stairs once discovered, so the player can explore the rest of the map before going down. So let’s allow some objects to be always visible as long as they’re on a tile that’s already been explored.

We can add always_visible to Object:

struct Object {
    x: i32,
    y: i32,
    // ...
    always_visible: bool,
}

Let’s initialise it to false in Object::new:

pub fn new(x: i32, y: i32, char: char, name: &str, color: Color, blocks: bool) -> Self {
    Object {
        x: x,
        y: y,
        // ...
        always_visible: false,
    }
}

Now update render_all to take it into account. When building the to_draw vector, let’s update the filter test to this:

let mut to_draw: Vec<_> = objects
    .iter()
    .filter(|o| {
        tcod.fov.is_in_fov(o.x, o.y)
            || (o.always_visible && game.map[o.x as usize][o.y as usize].explored)
    })
    .collect();

We keep the old is_in_fov test, but now we can also show the object if it’s always visible and on an explored tile.

So let’s set always_visible = true to stairs in make_map:

stairs.always_visible = true;

you will also have to add mut to let stairs a line above.

And let’s do the same for items, too! In place_objects before objects.push(item):

let dice = rand::random::<f32>();
let mut item = if dice < 0.7 {  (1)
    // create a healing potion (70% chance)
    let mut object = Object::new(x, y, '!', "healing potion", VIOLET, false);
    object.item = Some(Item::Heal);
    object
}
...
item.always_visible = true;
objects.push(item);
1 Item must be mut now

Character progression

With being able to go into deeper levels, the player character now feels a bit static. Let’s track their experience and allow to level up. We’ll put a new xp field into the Fighter struct:

struct Fighter {
    max_hp: i32,
    hp: i32,
    defense: i32,
    power: i32,
    xp: i32,  (1)
    on_death: DeathCallback,
}
1 Added the xp field

When setting the orc and trolls' Fighter component in place_objects, we’ll add 35 and 100 xp respectively. Feel free to plug your own values here.

let mut monster = if rand::random::<f32>() < 0.8 {
    // 80% chance of getting an orc
    // create an orc
    let mut orc = Object::new(x, y, 'o', "orc", DESATURATED_GREEN, true);
    orc.fighter = Some(Fighter {
        max_hp: 10,
        hp: 10,
        defense: 0,
        power: 3,
        xp: 35,  (1)
        on_death: DeathCallback::Monster,
    });
    orc.ai = Some(Ai::Basic);
    orc
} else {
    // create a troll
    let mut troll = Object::new(x, y, 'T', "troll", DARKER_GREEN, true);
    troll.fighter = Some(Fighter {
        max_hp: 16,
        hp: 16,
        defense: 1,
        power: 4,
        xp: 100,  (2)
        on_death: DeathCallback::Monster,
    });
    troll.ai = Some(Ai::Basic);
    troll
};
1 Defeating an orc gives you 35 XP
2 Defeating a troll gives you 100 XP

We’ll have to set player’s XP in new_game to something as well. Let’s put a 0 in and we’ll use it to track player’s experience.

player.fighter = Some(Fighter {
    max_hp: 30,
    hp: 30,
    defense: 2,
    power: 5,
    xp: 0,  (1)
    on_death: DeathCallback::Player,
});
1 Added xp

Now update take_damage to return the experience points when a monster is killed:

pub fn take_damage(&mut self, damage: i32, game: &mut Game) -> Option<i32> {  (1)
    // apply damage if possible
    if let Some(fighter) = self.fighter.as_mut() {
        if damage > 0 {
            fighter.hp -= damage;
        }
    }
    // check for death, call the death function
    if let Some(fighter) = self.fighter {
        if fighter.hp <= 0 {
            self.alive = false;
            fighter.on_death.callback(self, game);
            return Some(fighter.xp);  (2)
        }
    }
    None  (3)
}
1 May return a number of XP if take_damage killed the monster
2 We did kill the monster, return its XP
3 We did not kill the monster, don’t return anything

Now in attack, when an attacker kills their target, let’s increase their xp by replacing the target.take_damage(…​) call with:

if let Some(xp) = target.take_damage(damage, game) {
    // yield experience to the player
    self.fighter.as_mut().unwrap().xp += xp;
}

And we need to do the same in the two other places we’re calling take_damage. First in cast_lightning:

if let Some(xp) = objects[monster_id].take_damage(LIGHTNING_DAMAGE, game) {
    objects[PLAYER].fighter.as_mut().unwrap().xp += xp;
}

The cast_fireball function is going to be slightly trickier because we don’t want to give the player XP for burning themself and we can’t modify the player inside the loop because the objects Vec is already mutably borrowed.

So, whenever we get some XP from take_damage, we’ll add it to a variable and then give it all to the player afterwards:

let mut xp_to_gain = 0;  (1)
for (id, obj) in objects.iter_mut().enumerate() {  (2)
    if obj.distance(x, y) <= FIREBALL_RADIUS as f32 && obj.fighter.is_some() {
        game.messages.add(
            format!(
                "The {} gets burned for {} hit points.",
                obj.name, FIREBALL_DAMAGE
            ),
            ORANGE,
        );
        if let Some(xp) = obj.take_damage(FIREBALL_DAMAGE, game) {
            if id != PLAYER {  (3)
                // Don't reward the player for burning themself!
                xp_to_gain += xp;
            }
        }
    }
}
objects[PLAYER].fighter.as_mut().unwrap().xp += xp_to_gain;  (4)
1 Keep track of all the XP player should receive
2 Use enumerate to get the object’s index as well
3 Use the index to make sure we don’t include the player’s XP
4 Give all the accumulated XP to the player

Ok, so what can the player do with all this experience they’re getting now? Level up of course!

First, we’ll need to keep track of player’s level. We’ll add it as another field to Object (so that monsters and items can have levels too if we decide to use them later), but you could add it into Game just as easily.

struct Object {
    // ...
    level: i32,
}

And initialise it to 1 in Object’s new method:

    pub fn new(x: i32, y: i32, char: char, name: &str, color: Color, blocks: bool) -> Self {
        Object {
            // ...
            level: 1,
        }
    }

Typically, you need more experience to level up the higher you get. Let’s set the starting point to 350 xp and then require 150 for every new level. So the formula is 200 + player.level * 150.

Add constants so it can be easily tweaked later:

// experience and level-ups
const LEVEL_UP_BASE: i32 = 200;
const LEVEL_UP_FACTOR: i32 = 150;

Now the function that level’s the player up if they have enough experience:

fn level_up(tcod: &mut Tcod, game: &mut Game, objects: &mut [Object]) {
    let player = &mut objects[PLAYER];
    let level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR;
    // see if the player's experience is enough to level-up
    if player.fighter.as_ref().map_or(0, |f| f.xp) >= level_up_xp {
        // it is! level up
        player.level += 1;
        game.messages.add(
            format!(
                "Your battle skills grow stronger! You reached level {}!",
                player.level
            ),
            YELLOW,
        );
        // ... TODO increase players's stats!
    }
}

So, if the player has enough experience, we’ll increase their level and print out a message.

But let’s give them an actual gameplay bonus. Using the menu function, we’ll give them three choices: to increase the HP, attack or defense.

Put this in place of the TODO comment:

let fighter = player.fighter.as_mut().unwrap();
let mut choice = None;
while choice.is_none() {
    // keep asking until a choice is made
    choice = menu(
        "Level up! Choose a stat to raise:\n",
        &[
            format!("Constitution (+20 HP, from {})", fighter.max_hp),
            format!("Strength (+1 attack, from {})", fighter.power),
            format!("Agility (+1 defense, from {})", fighter.defense),
        ],
        LEVEL_SCREEN_WIDTH,
        &mut tcod.root,
    );
}
fighter.xp -= level_up_xp;
match choice.unwrap() {
    0 => {
        fighter.max_hp += 20;
        fighter.hp += 20;
    }
    1 => {
        fighter.power += 1;
    }
    2 => {
        fighter.defense += 1;
    }
    _ => unreachable!(),
}

We’ll need to add the new constant on top of the file and then it should compile:

const LEVEL_SCREEN_WIDTH: i32 = 40;

Now we can call level_up in the main loop (in play_game) after tcod.root.flush():

// level up if needed
level_up(tcod, game, objects);

So the player can now level up, but it would be great to show the current stats somewhere. Let’s display a little message box when the C key is pressed. In handle_keys:

(Key { code: Text, .. }, "c", true) => {
    // show character information
    let player = &objects[PLAYER];
    let level = player.level;
    let level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR;
    if let Some(fighter) = player.fighter.as_ref() {
        let msg = format!(
            "Character information

Level: {}
Experience: {}
Experience to level up: {}

Maximum HP: {}
Attack: {}
Defense: {}",
            level, fighter.xp, level_up_xp, fighter.max_hp, fighter.power, fighter.defense
        );
        msgbox(&msg, CHARACTER_SCREEN_WIDTH, &mut tcod.root);
    }

    DidntTakeTurn
}

This will build up a multiline string that we use msgbox to show. We’ll need to define the new constant at the top of the file and then it should all work:

const CHARACTER_SCREEN_WIDTH: i32 = 30;

It would also be nice if we showed how much XP did the player get for slaying a monster. We can modify the log message in monster_death:

// transform it into a nasty corpse! it doesn't block, can't be
// attacked and doesn't move
game.messages.add(
    format!(
        "{} is dead! You gain {} experience points.",
        monster.name,
        monster.fighter.unwrap().xp
    ),
    ORANGE,
);

Finally, completely unrelated to the character progression, but let’s add diagonal movement and sleep command using the keys on the numpad.

The key codes for the numpad keys are NumPad0 to NumPad9. So in handle_keys, we’ll replace the existing movement code with this:

// movement keys
(Key { code: Up, .. }, _, true) | (Key { code: NumPad8, .. }, _, true) => {
    player_move_or_attack(0, -1, game, objects);
    TookTurn
}
(Key { code: Down, .. }, _, true) | (Key { code: NumPad2, .. }, _, true) => {
    player_move_or_attack(0, 1, game, objects);
    TookTurn
}
(Key { code: Left, .. }, _, true) | (Key { code: NumPad4, .. }, _, true) => {
    player_move_or_attack(-1, 0, game, objects);
    TookTurn
}
(Key { code: Right, .. }, _, true) | (Key { code: NumPad6, .. }, _, true) => {
    player_move_or_attack(1, 0, game, objects);
    TookTurn
}
(Key { code: Home, .. }, _, true) | (Key { code: NumPad7, .. }, _, true) => {
    player_move_or_attack(-1, -1, game, objects);
    TookTurn
}
(Key { code: PageUp, .. }, _, true) | (Key { code: NumPad9, .. }, _, true) => {
    player_move_or_attack(1, -1, game, objects);
    TookTurn
}
(Key { code: End, .. }, _, true) | (Key { code: NumPad1, .. }, _, true) => {
    player_move_or_attack(-1, 1, game, objects);
    TookTurn
}
(Key { code: PageDown, .. }, _, true) | (Key { code: NumPad3, .. }, _, true) => {
    player_move_or_attack(1, 1, game, objects);
    TookTurn
}
(Key { code: NumPad5, .. }, _, true) => {
    TookTurn // do nothing, i.e. wait for the monster to come to you
}

Now we can use arrows and numpad to move around. And pressing 5 will let you skip a turn and have the monster come to you.

Continue to the next part.