The Components

TODO: talk about composition vs. inheritance and how this isn’t a real ECS whatever that means…​

Our components will be plain structs with related bits of data and not much else. Each Object will have some (or all or none) of the components attached and that will drive their behaviour. Only things with the Fighter component will be able to attack or be attacked, for example.

Let’s create the Fighter component. It will have hit points, maximum hit points (for healing), defense and attack power.

// combat-related properties and methods (monster, player, NPC).
#[derive(Clone, Copy, Debug, PartialEq)]
struct Fighter {
    max_hp: i32,
    hp: i32,
    defense: i32,
    power: i32,
}

Next is the component for monster artificial intelligence. For now, it will not carry any data, but we’ll soon remedy that.

#[derive(Clone, Debug, PartialEq)]
enum Ai {
    Basic,
}

And update the Object definition:

struct Object {
    x: i32,
    y: i32,
    char: char,
    color: Color,
    name: String,
    blocks: bool,
    alive: bool,
    fighter: Option<Fighter>,  (1)
    ai: Option<Ai>,  (2)
}
1 Added the Fighter component
2 Added the Ai component

and Object::new:

pub fn new(x: i32, y: i32, char: char, name: &str, color: Color, blocks: bool) -> Self {
    Object {
        x: x,
        y: y,
        char: char,
        color: color,
        name: name.into(),
        blocks: blocks,
        alive: false,
        fighter: None,  (1)
        ai: None,  (2)
    }
}
1 Initialise the Fighter component to None
2 Initialise the Ai component to None

This means that the newly-created objects will not have any components. We can add them ourselves, though!

First the player:

player.fighter = Some(Fighter {
    max_hp: 30,
    hp: 30,
    defense: 2,
    power: 5,
});

(you’ll need to make the player variable mutable because we’re changing it now)

And next the monsters. Each monster will get a Fighter component as well as the Ai one. In place_objects where the monsters are defined:

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,
    });
    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,
    });
    troll.ai = Some(Ai::Basic);
    troll
};

AI

We went through all this trouble and yet nothing happens? Let’s fix that by actually using our newly-minted components! The monsters have been growling for too long and are ready to fight now.

We’ll start by creating a function that will cause an object (monster, usually) to move towards a position (the player’s coordinates, usually).

fn move_towards(id: usize, target_x: i32, target_y: i32, map: &Map, objects: &mut [Object]) {
    // vector from this object to the target, and distance
    let dx = target_x - objects[id].x;
    let dy = target_y - objects[id].y;
    let distance = ((dx.pow(2) + dy.pow(2)) as f32).sqrt();

    // normalize it to length 1 (preserving direction), then round it and
    // convert to integer so the movement is restricted to the map grid
    let dx = (dx as f32 / distance).round() as i32;
    let dy = (dy as f32 / distance).round() as i32;
    move_by(id, dx, dy, map, objects);
}

Next we’ll add a method on Object that will tell us the distance to another object.

/// return the distance to another object
pub fn distance_to(&self, other: &Object) -> f32 {
    let dx = other.x - self.x;
    let dy = other.y - self.y;
    ((dx.pow(2) + dy.pow(2)) as f32).sqrt()
}

All right, let’s use them to implement some basic behaviour: if the monster is close, it will attack, otherwise it will move closer.

fn ai_take_turn(monster_id: usize, tcod: &Tcod, game: &Game, objects: &mut [Object]) {
    // a basic monster takes its turn. If you can see it, it can see you
    let (monster_x, monster_y) = objects[monster_id].pos();
    if tcod.fov.is_in_fov(monster_x, monster_y) {
        if objects[monster_id].distance_to(&objects[PLAYER]) >= 2.0 {
            // move towards player if far away
            let (player_x, player_y) = objects[PLAYER].pos();
            move_towards(monster_id, player_x, player_y, &game.map, objects);
        } else if objects[PLAYER].fighter.map_or(false, |f| f.hp > 0) {
            // close enough, attack! (if the player is still alive.)
            let monster = &objects[monster_id];
            println!(
                "The attack of the {} bounces off your shiny metal armor!",
                monster.name
            );
        }
    }
}

But for any of this to have effect, we need to call it from the main loop:

// let monsters take their turn
if objects[PLAYER].alive && player_action != PlayerAction::DidntTakeTurn {
    for id in 0..objects.len() {
        if objects[id].ai.is_some() {
            ai_take_turn(id, &tcod, &game, &mut objects);
        }
    }
}

When you test it now, you can see the monsters following you around and trying to attack you.

The whole code is available here.

Sword-fighting

The quest for some epic medieval combat is coming to an end! We will now write the actual functions to attack and take damage, and replace those silly placeholders with the meaty stuff. The "meaty stuff" is deliberately simple. This is so you can easily change it with your own damage system, whatever it may be.

pub fn take_damage(&mut self, damage: i32) {
    // apply damage if possible
    if let Some(fighter) = self.fighter.as_mut() {
        if damage > 0 {
            fighter.hp -= damage;
        }
    }
}

In the next section we’ll modify it to also handle deaths. Then there’s the method to attack another object:

pub fn attack(&mut self, target: &mut Object) {
    // a simple formula for attack damage
    let damage = self.fighter.map_or(0, |f| f.power) - target.fighter.map_or(0, |f| f.defense);
    if damage > 0 {
        // make the target take some damage
        println!(
            "{} attacks {} for {} hit points.",
            self.name, target.name, damage
        );
        target.take_damage(damage);
    } else {
        println!(
            "{} attacks {} but it has no effect!",
            self.name, target.name
        );
    }
}

It calls the previous method in order to handle taking damage. We separated "attacks" and "damage" because you might want an event, like poison or a trap, to directly damage an object by some amount, without going through the attack damage formula.

Let’s replace the dummy attack message in ai_take_turn with a call to the attack monster.

Alas, the ownership rears its head again! If you just tried the straightforward bit:

let monster = &mut objects[monster_id];
monster.attack(&mut objects[PLAYER]);

You would get another error about a double mutable borrow. While taking two mutable pointers into the objects list is safe when they’re pointing at different objects, it would be a problem if they borrowed the same one (remember, you can only have one mutable borrow to an object at a time).

Unfortunately, Rust can’t just figure out that the monster and player are different items in the list.

However, we can let it know! There’s a method on slices called split_at_mut which takes an index and returns two mutable slices split by the index. And we can use that to return a mutable borrow to our object from both:

/// Mutably borrow two *separate* elements from the given slice.
/// Panics when the indexes are equal or out of bounds.
fn mut_two<T>(first_index: usize, second_index: usize, items: &mut [T]) -> (&mut T, &mut T) {
    assert!(first_index != second_index);
    let split_at_index = cmp::max(first_index, second_index);
    let (first_slice, second_slice) = items.split_at_mut(split_at_index);
    if first_index < second_index {
        (&mut first_slice[first_index], &mut second_slice[0])
    } else {
        (&mut second_slice[0], &mut first_slice[second_index])
    }
}

And now monster’s attack looks like this:

// close enough, attack! (if the player is still alive.)
let (monster, player) = mut_two(monster_id, PLAYER, objects);
monster.attack(player);

And do the same to the player’s dummy attack code in player_move_or_attack:

let (player, target) = mut_two(PLAYER, target_id, objects);
player.attack(target);

That’s it, the player and the monsters can beat each other silly, but no-one will die. We’ll take this opportunity to print the player’s HP so you can see it plummeting to negative values as the monsters attack you. This is how you make a simple GUI! At the end of the render_all function:

// show the player's stats
tcod.root.set_default_foreground(WHITE);
if let Some(fighter) = objects[PLAYER].fighter {
    tcod.root.print_ex(
        1,
        SCREEN_HEIGHT - 2,
        BackgroundFlag::None,
        TextAlignment::Left,
        format!("HP: {}/{} ", fighter.hp, fighter.max_hp),
    );
}
We render the hitpoints only when the player has the Fighter component. We could use objects[PLAYER].fighter.unwrap() instead of if let here, but that would crash the game if the player ever stopped being a fighter, which would be a shame. What if they’re under a sanctuary spell or some such?

Untimely deaths

Of course, nobody can lose HP indefinitely. We’ll now code the inevitable demise of both the monsters and the player! This is handled by the Fighter component. Since different objects have different behaviors when killed, the Fighter struct must know what function to call when the object dies. This is so that monsters leave corpses behind, the player loses the game, the end-level boss reveals the stairs to the next level, etc. This on_death callback is passed as a parameter when creating a Fighter instance.

// combat-related properties and methods (monster, player, NPC).
#[derive(Clone, Copy, Debug, PartialEq)]
struct Fighter {
    max_hp: i32,
    hp: i32,
    defense: i32,
    power: i32,
    on_death: DeathCallback,  (1)
}
1 New on_death callback field

Let us define the callback as well:

#[derive(Clone, Copy, Debug, PartialEq)]
enum DeathCallback {
    Player,
    Monster,
}

We’re adding another field to Fighter of a new enum DeathCallback. It will represent the different "on death" functions we’ll have available.

Next, we’ll add a method that will let us call the callback:

impl DeathCallback {
    fn callback(self, object: &mut Object) {
        use DeathCallback::*;
        let callback: fn(&mut Object) = match self {
            Player => player_death,
            Monster => monster_death,
        };
        callback(object);
    }
}

It checks to see which callback it represents and invokes the right function (player_death or monster_death). The callback functions take one parameter — the mutable reference to the dying object. This is so we can change its properties on death.

And we also need to set the callback for every Fighter instance. Here’s the player’s one:

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

And this is for the monsters (in place_objects):

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,
        on_death: DeathCallback::Monster,  (1)
    });
    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,
        on_death: DeathCallback::Monster,  (2)
    });
    troll.ai = Some(Ai::Basic);
    troll
};
1 Added on_death callback
2 Added on_death callback

Before we get to writing the concrete callback implementations, lets make sure our they actually get triggered when an object dies!

We’ll do that in take_damage rather than attack, because an object may die from causes other than combat, such as a trap, hunger or poison.

Put this at the end of the take_damage method:

// apply damage if possible
if let Some(fighter) = self.fighter.as_mut() {
    // ...
}
// 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);
    }
}

The first if let check looks almost identical to the one that’s already there for taking the hit points down. There is a difference, however.

It boils down to ownership again. The first if let takes a mutable reference to self.fighter. That means, for the duration of that block, we can’t take a mutable reference to self, because a part of it (fighter) is already borrowed.

But we do need a mutable reference to pass it to the on_death callback.

So while it may seem like we could just fold the callback code into the first if let, we can’t because it would result in the simultaneous borrowing of &mut Object and &mut Fighter.

We do not have the same problem in the second if let because we are not borrowing Fighter there. Using self.fighter instead of self.fighter.as_mut() means we just copy the fighter value, but nothing is borrowed at that time. This would also mean that if we made any changes to fighter in the second if let block, they would not appear on the self Object.

As mentioned before, the ownership rules are generally a good thing but sometimes they are a bit onerous.

Anyway, let’s go implement our player_death and monster_death callbacks!

fn player_death(player: &mut Object) {
    // the game ended!
    println!("You died!");

    // for added effect, transform the player into a corpse!
    player.char = '%';
    player.color = DARK_RED;
}

fn monster_death(monster: &mut Object) {
    // transform it into a nasty corpse! it doesn't block, can't be
    // attacked and doesn't move
    println!("{} is dead!", monster.name);
    monster.char = '%';
    monster.color = DARK_RED;
    monster.blocks = false;
    monster.fighter = None;
    monster.ai = None;
    monster.name = format!("remains of {}", monster.name);
}

Notice that the monster’s components were disabled, so it doesn’t run any AI functions and can no longer be attacked.

To enable these behaviours, pass the on_death field into the Fighter components wherever you’ve defined them. Rust will complain if you don’t so let the compiler guide you.

You can test play around with it now and you’ll see that the player and monsters stop moving when they die. There are some glitches we need to fix, however.

First, we only want to attack an object if it has a Fighter component. In player_move_or_attack, change the target check to the following:

// try to find an attackable object there
let target_id = objects
    .iter()
    .position(|object| object.fighter.is_some() && object.pos() == (x, y));

is_some is a method on Option that will tell you whether it’s value is Some(…​) without bothering you with the insides.

There’s also the issue that when the player walks over a corpse, it’s sometimes drawn over the player. And the same issue happens when a monster steps on a corpse.

We can fix both by sorting the list of objects by their blocks property:

let mut to_draw: Vec<_> = objects.iter().collect();
// sort so that non-blocking objects come first
to_draw.sort_by(|o1, o2| { o1.blocks.cmp(&o2.blocks) });
// draw the objects in the list
for object in &to_draw {
    if tcod.map.is_in_fov(object.x, object.y) {
        object.draw(con);
    }
}

Instead of going through the objects list we clone it into a mutable vector (render_all is taking &[Object] so it can’t change the list directly — nor should it). Then we sort the vector such that all non-blocking objects come before all the blocking ones. Since we can’t have two blocking objects on the same tile, this will make sure that our player and monsters won’t get overwritten by corpses.

And we can always make the logic more intricate by changing the closure passed to sort_by.

One more thing, since we’re only ever rendering objects that are in the field of view, let’s filter them out before the sort. That way we’ll only sort items that we actually want to draw.

let mut to_draw: Vec<_> = objects
    .iter()
    .filter(|o| tcod.fov.is_in_fov(o.x, o.y))
    .collect();
// sort so that non-blocking objects come first
to_draw.sort_by(|o1, o2| o1.blocks.cmp(&o2.blocks));
// draw the objects in the list
for object in &to_draw {
    object.draw(&mut tcod.con);
}

It’s finally ready to play, and it actually feels like a game! It’s been a long journey since we first printed the @ character, but we’ve got random dungeons, FOV, exploration, enemies, AI, and a true combat system. You can now beat those pesky monsters into a pulp and walk over them! (Ugh!) See if you can finish off all of them before they do the same to you.

Continue to the next part.