The inventory we implemented has lots of untapped potential; I encourage you to explore it fully and create as many different items as possible! (Though the special category of items that can be equipped, like swords and armor, will be done in a later section.) To get the ball rolling, we’ll start with some magic scrolls the player can use to cast spells. We’ll sample a few different spell types; I’m sure you can then create tons of variants from these little examples.

Magic scrolls: the Lightning Bolt

The first spell is a lightning bolt that damages the nearest enemy. It’s simple to code because it doesn’t involve any targeting. On the other hand, it creates some interesting tactical challenges: if the nearest enemy is not the one you want to hit (for example, it’s too weak to waste the spell on it), you have to maneuver into a good position. Just modify the place_objects function to choose at random between a healing potion and a lightning bolt scroll, the same way it’s done with monsters:

if !is_blocked(x, y, map, objects) {
    let dice = rand::random::<f32>();
    let item = if dice < 0.7 {
        // create a healing potion (70% chance)
        let mut object = Object::new(x, y, '!', "healing potion", VIOLET, false);
        object.item = Some(Item::Heal);
        object
    } else {
        // create a lightning bolt scroll (30% chance)
        let mut object = Object::new(
            x,
            y,
            '#',
            "scroll of lightning bolt",
            LIGHT_YELLOW,
            false,
        );
        object.item = Some(Item::Lightning);
        object
    };
    objects.push(item);
}

This means we have to add a new variant to the Item enum: Lightning. And we need to add a new on_use variant to the use_item (the compiler will complain if we don’t). So let’s do that:

let on_use = match item {
    Heal => cast_heal,
    Lightning => cast_lightning,
};

Now we need to write the cast_lightning function!

fn cast_lightning(
    _inventory_id: usize,
    tcod: &mut Tcod,
    game: &mut Game,
    objects: &mut [Object],
) -> UseResult {
    // find closest enemy (inside a maximum range and damage it)
    let monster_id = closest_monster(tcod, objects, LIGHTNING_RANGE);
    if let Some(monster_id) = monster_id {
        // zap it!
        game.messages.add(
            format!(
                "A lightning bolt strikes the {} with a loud thunder! \
                 The damage is {} hit points.",
                objects[monster_id].name, LIGHTNING_DAMAGE
            ),
            LIGHT_BLUE,
        );
        objects[monster_id].take_damage(LIGHTNING_DAMAGE, game);
        UseResult::UsedUp
    } else {
        // no enemy found within maximum range
        game.messages
            .add("No enemy is close enough to strike.", RED);
        UseResult::Cancelled
    }
}

It’s a plain spell but an imaginative message can always give it some flavor! It returns uses the UseResult::Cancelled if cancelled to prevent the item from being destroyed in that case, like the healing potion. There are also a couple of new constants that have to be defined:

const LIGHTNING_DAMAGE: i32 = 40;
const LIGHTNING_RANGE: i32 = 5;

Now let’s write closest_monster:

/// find closest enemy, up to a maximum range, and in the player's FOV
fn closest_monster(tcod: &Tcod, objects: &[Object], max_range: i32) -> Option<usize> {
    let mut closest_enemy = None;
    let mut closest_dist = (max_range + 1) as f32; // start with (slightly more than) maximum range

    for (id, object) in objects.iter().enumerate() {
        if (id != PLAYER)
            && object.fighter.is_some()
            && object.ai.is_some()
            && tcod.fov.is_in_fov(object.x, object.y)
        {
            // calculate distance between this object and the player
            let dist = objects[PLAYER].distance_to(object);
            if dist < closest_dist {
                // it's closer, so remember it
                closest_enemy = Some(id);
                closest_dist = dist;
            }
        }
    }
    closest_enemy
}

We just need to loop through all the monsters, and keep track of the closest one so far (and its distance). By initializing that distance at a bit more than the maximum range, any monster farther away is rejected. We also check that it’s in FOV, so the player can’t cast a spell through walls.

This makes use of the distance_to method we wrote earlier for the AI. Alright, the lightning bolt is done! If you have one you can take down a Troll with a single hit, sparing you from a lot of damage.

Spells that manipulate monsters: Confusion

There are many direct damage variants of the Lightning Bolt spell. So we’ll move on to a different sort of spell: one that affects the monsters' actions. This can be done by replacing their AI with a different one, that makes it do something different — run away in fear, stay knocked out for a few turns, even fight on the player’s side for a while!

My choice was a Confusion spell, that makes the monster move around randomly, and not attack the player. To do this, we’ll change our empty Ai struct into an enum with two variants:

#[derive(Clone, Debug, PartialEq)]
enum Ai {
    Basic,
    Confused {
        previous_ai: Box<Ai>,
        num_turns: i32,
    },
}

The Basic option is the AI we’ve used until now — a monster moves towards a player and tries to attack.

The Confused one is what we want to implement now: it moves randomly for a few turns and then reverts back to the AI it had before it got confused.

This is still an enum, but it uses a struct-like enum variant for Confused. In Rust, enums variants aren’t just empty identifiers, but can hold data, too!

We need to change the monster creation in place_objects a little:

// create an orc
orc.ai = Some(Ai::Basic);
// ...
// create a troll
troll.ai = Some(Ai::Basic);

Next, let’s move the code from ai_take_turn to its own function:

fn ai_take_turn(monster_id: usize, tcod: &Tcod, game: &mut Game, objects: &mut [Object]) {
    use Ai::*;
    if let Some(ai) = objects[monster_id].ai.take() {
        let new_ai = match ai {
            Basic => ai_basic(monster_id, tcod, game, objects),
            Confused {
                previous_ai,
                num_turns,
            } => ai_confused(monster_id, tcod, game, objects, previous_ai, num_turns),
        };
        objects[monster_id].ai = Some(new_ai);
    }
}

fn ai_basic(monster_id: usize, tcod: &Tcod, game: &mut Game, objects: &mut [Object]) -> Ai {
    // 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, player) = mut_two(monster_id, PLAYER, objects);
            monster.attack(player, game);
        }
    }
    Ai::Basic
}

The function now does a dispatch similar to the one in use_item. Based on the AI type, it calls ai_basic or ai_confused.

The ai_basic function now contains what was previously in ai_take_turn except that now it also returns an Ai value. This is because the Ai now can’t be Copy (and that’s because the Confused variant uses Box<Ai> and boxes cannot be copied).

In the case of the Basic ai, we don’t really care since we’re not modifying any data.

But in case of Confused, we’ll want to decrease the number of remaining turns and when they run out, swap the previous AI.

A simple way to do that without running into any ownership issues is to take the present Ai value (by calling ai.take() — it moves it out, leaving None in its place), calling the appropriate function (ai_basic or ai_confuse) with all its contents (i.e. previous_ai and num_turns for Confused) and then put whatever Ai the function returned back as the monster’s ai component.

It’s a bit complex if you haven’t internalised the Option and Box types and how the ownership works, but it’s actually quite straightforward once you do.

You can try to write ai_take_turn yourself without moving anything — just use objects[monster_id].ai.as_mut() to get a mutable reference and think through the compile errors Rust will give you.

So after that mouthful, the rather anti-climactic implementation of ai_confused:

fn ai_confused(
    monster_id: usize,
    _tcod: &Tcod,
    game: &mut Game,
    objects: &mut [Object],
    previous_ai: Box<Ai>,
    num_turns: i32,
) -> Ai {
    if num_turns >= 0 {
        // still confused ...
        // move in a random direction, and decrease the number of turns confused
        move_by(
            monster_id,
            rand::thread_rng().gen_range(-1, 2),
            rand::thread_rng().gen_range(-1, 2),
            &game.map,
            objects,
        );
        Ai::Confused {
            previous_ai: previous_ai,
            num_turns: num_turns - 1,
        }
    } else {
        // restore the previous AI (this one will be deleted)
        game.messages.add(
            format!("The {} is no longer confused!", objects[monster_id].name),
            RED,
        );
        *previous_ai
    }
}

It takes pretty much the same parameters as ai_basic, but it moves the monster at random if it’s still confused and it returns the previous AI otherwise.

If you look at the return values, in the confused case, we’re reconstructing the Ai::Confused value again, with the same previous_ai and a num_turns decreased by one. This is where we move previous_ai instead of mutating anything.

And in the else case, we just return previous_ai on its own, getting rid of the Confused value entirely. We have to prepend it with an asterisk to return the boxed value — Ai. If we didn’t put the asterisk there, we’d return Box<Ai>, which is not what ai_take_turn expects.

Now, the actual scroll that causes this AI! For it to appear in the dungeon it must be added to place_objects. Notice that the chance of getting a lightning bolt scroll must change:

...
} else if dice < 0.7 + 0.1 {
    // create a lightning bolt scroll (10% chance)
    let mut object =
        Object::new(x, y, '#', "scroll of lightning bolt", LIGHT_YELLOW, false);
    object.item = Some(Item::Lightning);
    object
} else {
    // create a confuse scroll (10% chance)
    let mut object = Object::new(x, y, '#', "scroll of confusion", LIGHT_YELLOW, false);
    object.item = Some(Item::Confuse);
    object
};

We’re making all scrolls look the same here, but in your game that’s up to you. The cast_confuse function can now be defined. It hits the closest monster for now, like the lightning bolt; later we’ll allow targeting.

The percentages in the comments aren’t quite correct right now, but they will be once we’ve added all the items here.
fn cast_confuse(
    _inventory_id: usize,
    tcod: &mut Tcod,
    game: &mut Game,
    objects: &mut [Object],
) -> UseResult {
    // find closest enemy in-range and confuse it
    let monster_id = target_monster(CONFUSE_RANGE, objects, tcod);
    if let Some(monster_id) = monster_id {
        let old_ai = objects[monster_id].ai.take().unwrap_or(Ai::Basic);
        // replace the monster's AI with a "confused" one; after
        // some turns it will restore the old AI
        objects[monster_id].ai = Some(Ai::Confused {
            previous_ai: Box::new(old_ai),
            num_turns: CONFUSE_NUM_TURNS,
        });
        game.messages.add(
            format!(
                "The eyes of {} look vacant, as he starts to stumble around!",
                objects[monster_id].name
            ),
            LIGHT_GREEN,
        );
        UseResult::UsedUp
    } else {
        // no enemy fonud within maximum range
        game.messages
            .add("No enemy is close enough to strike.", RED);
        UseResult::Cancelled
    }
}

We find the closest enemy again, extract its existing AI and replace it with the Confused one.

target_monster should always return a monster that has the Ai component, but the Object.ai still contains Option<Ai> rather than bare Ai (not every Object has AI even though we expect each monster to have one). We could use the unwrap or expect methods to get the inner value, but this would crash the program (expect would print a custom message). Here we use unwrap_or instead which will return the Basic AI in case there is none.

You may choose to panic with unwrap/expect instead (to find the bug early and hunt it down) or log the error and keep going or even allow monsters without AI and just handle that case properly!

We’ve also introduced two new constants:

const CONFUSE_RANGE: i32 = 8;
const CONFUSE_NUM_TURNS: i32 = 10;

Finally, to tie it all together, we need to add a new item type: Confuse:

#[derive(Clone, Copy, Debug, PartialEq)]
enum Item {
    Heal,
    Lightning,
    Confuse,
}

And associate it with cast_confuse in the use_item function:

let on_use = match item {
    Heal => cast_heal,
    Lightning => cast_lightning,
    Confuse => cast_confuse,
};

Targeting: the Fireball

Given that we know how to make direct damage spells like Lightning Bolt, others like Blizzard or Fireball are just a matter of finding all monsters in an area and damaging them; you should have no trouble creating them. But it would be much more interesting if the player could choose the target properly, and that’s a feature that will benefit many spells. In addition, you can use the same system for ranged weapons like crossbows or slings. So let’s do that!

We’re going to build a mouse interface. It’s also possible to make a classic keyboard interface, but it would be less intuitive and a bit harder to code; if you prefer that, consider it a small challenge!

We already have some code for getting the coordinates of the mouse, and checking for left-clicks is trivial — when it happens mouse.lbutton_pressed is true. So we just need to loop until the player clicks somewhere. By redrawing the screen with every loop, the names of objects under the mouse are automatically shown, and we erase the inventory from which the player chose the scroll (otherwise it would still be visible).

/// return the position of a tile left-clicked in player's FOV (optionally in a
/// range), or (None,None) if right-clicked.
fn target_tile(
    tcod: &mut Tcod,
    game: &mut Game,
    objects: &[Object],
    max_range: Option<f32>,
) -> Option<(i32, i32)> {
    use tcod::input::KeyCode::Escape;
    loop {
        // render the screen. this erases the inventory and shows the names of
        // objects under the mouse.
        tcod.root.flush();
        let event = input::check_for_event(input::KEY_PRESS | input::MOUSE).map(|e| e.1);
        match event {
            Some(Event::Mouse(m)) => tcod.mouse = m,
            Some(Event::Key(k)) => tcod.key = k,
            None => tcod.key = Default::default(),
        }
        render_all(tcod, game, objects, false);

        let (x, y) = (tcod.mouse.cx as i32, tcod.mouse.cy as i32);

        // ...
    }
}

We have to flush the console to present the changes to the player.

Now we return the clicked position if it’s in range and visible:

// accept the target if the player clicked in FOV, and in case a range
// is specified, if it's in that range
let in_fov = (x < MAP_WIDTH) && (y < MAP_HEIGHT) && tcod.fov.is_in_fov(x, y);
let in_range = max_range.map_or(true, |range| objects[PLAYER].distance(x, y) <= range);
if tcod.mouse.lbutton_pressed && in_fov && in_range {
    return Some((x, y));
}

The is_in_fov method expects that x and y are within the map’s bounds so we need to check for that.

If the max_range is none, we allow any range (so we make max_range.map_or return true), otherwise we need to check that the range from the clicked position to the player is lower or equal.

We also make sure that the target is within FOV to prevent firing through walls.

Finally, we need a way to cancel the targeting UI:

if tcod.mouse.rbutton_pressed || tcod.key.code == Escape {
    return None; // cancel if the player right-clicked or pressed Escape
}

This returns None if the player pressed Esc or clicked the right mouse button. If they didn’t do any of that, the loop continues.

Next we add a method to Object for calculating a distance to a specific coordinate (we already have one for distance between two objects).

/// return the distance to some coordinates
pub fn distance(&self, x: i32, y: i32) -> f32 {
    (((x - self.x).pow(2) + (y - self.y).pow(2)) as f32).sqrt()
}

That’s all for targeting a tile! We can now create a simple fireball spell:

fn cast_fireball(
    _inventory_id: usize,
    tcod: &mut Tcod,
    game: &mut Game,
    objects: &mut [Object],
) -> UseResult {
    // ask the player for a target tile to throw a fireball at
    game.messages.add(
        "Left-click a target tile for the fireball, or right-click to cancel.",
        LIGHT_CYAN,
    );
    let (x, y) = match target_tile(tcod, game, objects, None) {
        Some(tile_pos) => tile_pos,
        None => return UseResult::Cancelled,
    };
    game.messages.add(
        format!(
            "The fireball explodes, burning everything within {} tiles!",
            FIREBALL_RADIUS
        ),
        ORANGE,
    );

    for obj in objects {
        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,
            );
            obj.take_damage(FIREBALL_DAMAGE, game);
        }
    }

    UseResult::UsedUp
}

With some new constants:

const FIREBALL_RADIUS: i32 = 3;
const FIREBALL_DAMAGE: i32 = 12;

This also uses the new distance method. A scroll that casts the Fireball spell must be added to place_objects, before the Confuse scroll:

} else if dice < 0.7 + 0.1 + 0.1 {
    // create a fireball scroll (10% chance)
    let mut object = Object::new(x, y, '#', "scroll of fireball", LIGHT_YELLOW, false);
    object.item = Some(Item::Fireball);
    object
}  else {
    // create a confuse scroll (10% chance)
    // ...
}

And change all the "15%" and ".15" to "10%" now since there are three scrolls now, each with a 10% of appearing.

If we try to compile it now, Rust will complain that there is no Fireball variant for Item. So let’s add it:

#[derive(Clone, Copy, Debug, PartialEq)]
enum Item {
    Heal,
    Lightning,
    Confuse,
    Fireball,
}

Next, the item is missing from the match inside use_item, so let’s fix that:

let on_use = match item {
    Heal => cast_heal,
    Lightning => cast_lightning,
    Confuse => cast_confuse,
    Fireball => cast_fireball,
};

And finally, we’re expecting to pass &mut Map to cast_fireball (because target_tile requires it), but none of the other spells required it yet. Since they all must have the same function signature, we have to add it to cast_heal, cast_lightning, cast_confuse as well as use_item.

Here’s what the on_use bit looks like now:

let on_use = match item {
    Heal => cast_heal,
    Lightning => cast_lightning,
    Confuse => cast_confuse,
    Fireball => cast_fireball,
};
match on_use(inventory_id, tcod, game, objects) {
    // ...
}

To get this compiling will require us to pass map to a few more places along the call chain as well. Again, let the compiler guide you.

And now you can now pick up Fireball scrolls; they’re quite handy to roast large groups of Orcs! Try not to get burnt though, it also damages the player. I think it adds some strategic value, balancing the spell.

If you do want the player to be immune, you can add enumerate to the for loop and check whether the id is different from PLAYER:

for (id, obj) in objects.iter_mut().enumerate() {
    if obj.distance(x, y) <= FIREBALL_RADIUS as f32 && obj.fighter.is_some() && id != PLAYER {
        // ...
    }
}

Targeting single monsters

Let’s not stop there! Area spells like the Fireball are fine, but many spells affect single monsters. Can we make a handy function to target a single monster? Sure! It will simply wrap target_tile and stop only when a monster is selected.

/// returns a clicked monster inside FOV up to a range, or None if right-clicked
fn target_monster(
    tcod: &mut Tcod,
    game: &mut Game,
    objects: &[Object],
    max_range: Option<f32>,
) -> Option<usize> {
    loop {
        match target_tile(tcod, game, objects, max_range) {
            Some((x, y)) => {
                // return the first clicked monster, otherwise continue looping
                for (id, obj) in objects.iter().enumerate() {
                    if obj.pos() == (x, y) && obj.fighter.is_some() && id != PLAYER {
                        return Some(id);
                    }
                }
            }
            None => return None,
        }
    }
}

The Confuse spell is a bit weak, since monsters that move randomly can be hard to hit before the spell runs out. So we’ll compensate a bit by letting the player choose any target for it; conveniently testing our new function. Just replace the first 2 lines of the cast_confuse function with:

// ask the player for a target to confuse
game.messages.add(
    "Left-click an enemy to confuse it, or right-click to cancel.",
    LIGHT_CYAN,
);
let monster_id = target_monster(tcod, game, objects, Some(CONFUSE_RANGE as f32));

Dropping items

Right, there’s an inventory feature that didn’t make it into Part 8, since it was getting too long. You’ll miss it when you hit the maximum number of items in your inventory: dropping items. A new function will do that. To drop an item you just add it to the map’s objects and remove it from the inventory. Then you must set its coordinates to the player’s, so it appears below the player:

fn drop_item(inventory_id: usize, game: &mut Game, objects: &mut Vec<Object>) {
    let mut item = game.inventory.remove(inventory_id);
    item.set_pos(objects[PLAYER].x, objects[PLAYER].y);
    game.messages
        .add(format!("You dropped a {}.", item.name), YELLOW);
    objects.push(item);
}

To let the player choose an item to drop, we’ll call the inventory_menu function when the player presses the D key, then drop the chosen item. Add this to handle_keys, after the inventory key:

(Key { code: Text, .. }, "d", true) => {
    // show the inventory; if an item is selected, drop it
    let inventory_index = inventory_menu(
        &game.inventory,
        "Press the key next to an item to drop it, or any other to cancel.\n'",
        &mut tcod.root,
    );
    if let Some(inventory_index) = inventory_index {
        drop_item(inventory_index, game, objects);
    }
    DidntTakeTurn
}

Some new spells, targeting, dropping items — that’s enough for now! See how the spells affect your strategy, they’ll surely make things much more interesting!

Continue to the next part.