Equipment


About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Hands-On Rust


In the last chapter, we moved to a d20-style (D&D-like) combat system and attributes system. It's functional, but doesn't really give any opportunity to better your character through gear. Finding cool items, and painstakingly maximizing your efficiency is a bedrock of roguelike games - providing a lot of depth, and a feeling that while the game is random you can heavily influence it to get the results you desire.

Parsing Dice Strings

We're going to find it very helpful to be able to read a string containing a D&D dice specification (e.g. 20d6+4) and turn it into computer-friendly numbers. We'll use this a lot when reading the raw files, so we'll put it in there - but make it public in case we need it somewhere else.

Parsing out bit of text like that is a perfect job for regular expressions. These are supported in Rust via a crate, so we have to open up our cargo.toml and add regex = "1.3.6" to our [dependencies] section. Actually teaching regular expressions would be a book unto itself; it's a hugely complicated (and powerful) system, and has a tendency to look like a cat walked on your keyboard. Here's a regular expression that parses a 1d20+4 type of string:

(\d+)d(\d+)([\+\-]\d+)?

What on Earth does that mean?

  • Each part contained in parentheses (..) is a match group. You're telling the regular expression that whatever is in those parentheses is important to you, and can be captured for reading.
  • \d is regular-expression speak for "I expect a digit here".
  • Adding a + means "there may be more than one digit here, keep reading until you hit something else.
  • Therefore, the first (\d+) means "capture all the digits at the front of the string".
  • The d outside of a group is a literal d character. So we separate the first set of numbers from subsequent parts with the letter d. Now we're up to 1d.
  • The next (\d+) works the same way - keep reading digits in, and capture them into the second group. So now we've read up to 1d20 and captured 1 and 20 in groups.
  • The last group is a bit more confusing. [..] means "expect any one of these characters". The backslash (\) is escaping the subsequent character, meaning "+ or - can mean something in regular expression language; in this case, please just treat it as a symbol". So [\+\-] means "expect a plus or a minus here". Then we read however many digits are there.
  • So now we've got 1d20+4 broken into 1, 20, and 4.

It's entirely possible that we'll pass a dice type without a +4, e.g. 1d20. In this case, the regex will match the 1 and the 20 - but the last group will be empty.

Here's the Rust for our function:


#![allow(unused)]
fn main() {
pub fn parse_dice_string(dice : &str) -> (i32, i32, i32) {
    lazy_static! {
        static ref DICE_RE : Regex = Regex::new(r"(\d+)d(\d+)([\+\-]\d+)?").unwrap();
    }
    let mut n_dice = 1;
    let mut die_type = 4;
    let mut die_bonus = 0;
    for cap in DICE_RE.captures_iter(dice) {
        if let Some(group) = cap.get(1) {
            n_dice = group.as_str().parse::<i32>().expect("Not a digit");
        }
        if let Some(group) = cap.get(2) {
            die_type = group.as_str().parse::<i32>().expect("Not a digit");
        }
        if let Some(group) = cap.get(3) {
            die_bonus = group.as_str().parse::<i32>().expect("Not a digit");
        }

    }
    (n_dice, die_type, die_bonus)
}
}

Well, that's as clear as mud. Let's walk through and try to decipher it a bit.

  1. Regular expressions are compiled into their own internal format when first parsed by the Rust Regex library. We don't want to do this every time we try to read a dice string, so we take the advice of the Rust Cookbook and bake reading the expression into a lazy_static! (like we used for globals). This way, it'll be parsed just once and the regular expression is ready to use when we need it.
  2. We set some mutable variables to the different parts of the dice expression; the number of dice, their type (number of sides) and bonus (which will be negative for a penalty). We give them some defaults in case we have troubles reading the string (or parts thereof).
  3. Now we use the captures_iter feature of the regex library; we pass it the string we are looking at, and it returns an iterator of all captures (complicated regular expressions might have lots of these). In our case, this returns one capture set, which may contain all of the groups we discussed above.
  4. Now, it's possible that any of the groups won't exist. So we do an if let on each capture group. If it does exist, we retrieve the string with as_str and parse it into an integer - and assign it to the right part of the dice reader.
  5. We return all the parts as a tuple.

Defining melee weaponry

For now, there's no need to change consumables - the system works ok. We're going to focus on equippable items: those you can wield, wear or otherwise benefit from. Our previous definition of a "dagger" looked like this:

{
    "name" : "Dagger",
    "renderable": {
        "glyph" : "/",
        "fg" : "#FFAAAA",
        "bg" : "#000000",
        "order" : 2
    },
    "weapon" : {
        "range" : "melee",
        "power_bonus" : 2
    }
},

The power_bonus is now outmoded; weapons don't work that way anymore. Instead, we want to be able to define D&D-like stats for them. Here's a modernized dagger:

{
    "name" : "Dagger",
    "renderable": {
        "glyph" : "/",
        "fg" : "#FFAAAA",
        "bg" : "#000000",
        "order" : 2
    },
    "weapon" : {
        "range" : "melee",
        "attribute" : "Quickness",
        "base_damage" : "1d4",
        "hit_bonus" : 0
    }
},

To support this, in raws/item_structs.rs we change the Weapon struct:


#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug)]
pub struct Weapon {
    pub range: String,
    pub attribute: String,
    pub base_damage: String,
    pub hit_bonus: i32
}
}

Now open components.rs, and we'll change MeleePowerBonus (and rename it from main.rs and saveload_system.rs). We're going to replace it with MeleeWeapon that captures these aspects, but in a more machine-friendly format (so we aren't parsing strings all the time):


#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum WeaponAttribute { Might, Quickness }

#[derive(Component, Serialize, Deserialize, Clone)]
pub struct MeleeWeapon {
    pub attribute : WeaponAttribute,
    pub damage_n_dice : i32,
    pub damage_die_type : i32,
    pub damage_bonus : i32,
    pub hit_bonus : i32
}
}

We've condensed attribute into an enum (much faster to read), and broken 1d4+0 into meaning: (1) damage_n_dice, (4) damage_die_type, plus damage_bonus.

We'll also need to change spawn_named_item in raws/rawmaster.rs:


#![allow(unused)]
fn main() {
if let Some(weapon) = &item_template.weapon {
    eb = eb.with(Equippable{ slot: EquipmentSlot::Melee });
    let (n_dice, die_type, bonus) = parse_dice_string(&weapon.base_damage);
    let mut wpn = MeleeWeapon{
        attribute : WeaponAttribute::Might,
        damage_n_dice : n_dice,
        damage_die_type : die_type,
        damage_bonus : bonus,
        hit_bonus : weapon.hit_bonus
    };
    match weapon.attribute.as_str() {
        "Quickness" => wpn.attribute = WeaponAttribute::Quickness,
        _ => wpn.attribute = WeaponAttribute::Might
    }
    eb = eb.with(wpn);
}
}

That should be enough to read in our nicer weapons format, and have them usable in-game.

Starting with a weapon

If you go back to the design document, we stated that you start with some minimal equipment. We'll let you start with your father's rusty longsword. Let's add this to the spawns.json file:

{
    "name" : "Rusty Longsword",
    "renderable": {
        "glyph" : "/",
        "fg" : "#BB77BB",
        "bg" : "#000000",
        "order" : 2
    },
    "weapon" : {
        "range" : "melee",
        "attribute" : "Might",
        "base_damage" : "1d8-1",
        "hit_bonus" : -1
    }
},

We've darkened the color a bit (it is rusty, after all) and added a -1 penalty to the sword (to account for its condition). Now, we want it to start with the player. Currently in spawners.rs, our player function makes the player - and nothing else. We want him/her to start with some initial equipment. Currently, we only allow for spawning items on the ground; that won't do (you'd have to remember to pick it up when you start!) - so we'll expand our raw file spawning system to handle it (that's why we had the SpawnType enumeration in mod/rawmaster.rs - even if it only had one entry!). Let's add Equipped and Carried to that enum:


#![allow(unused)]
fn main() {
pub enum SpawnType {
    AtPosition { x: i32, y: i32 },
    Equipped { by: Entity },
    Carried { by: Entity }
}
}

We're going to need a function to figure out what slot an equipped item should go into. This will do the trick:


#![allow(unused)]
fn main() {
fn find_slot_for_equippable_item(tag : &str, raws: &RawMaster) -> EquipmentSlot {
    if !raws.item_index.contains_key(tag) {
        panic!("Trying to equip an unknown item: {}", tag);
    }
    let item_index = raws.item_index[tag];
    let item = &raws.raws.items[item_index];
    if let Some(_wpn) = &item.weapon {
        return EquipmentSlot::Melee;
    } else if let Some(wearable) = &item.wearable {
        return string_to_slot(&wearable.slot);
    }
    panic!("Trying to equip {}, but it has no slot tag.", tag);
}
}

Notice that we're explicitly calling panic! for conditions that could result in really weird/unexpected game behavior. Now you have no excuse not to be careful with your raw file entries! It's pretty simple: it looks up the item name in the index, and uses that to look up the item. If its a weapon, it derives the slot from that (currently always Melee). If its a wearable, it uses out string_to_slot function to calculate from that.

We'll also need to update the spawn_position function to handle this:


#![allow(unused)]
fn main() {
fn spawn_position<'a>(pos : SpawnType, new_entity : EntityBuilder<'a>, tag : &str, raws: &RawMaster) -> EntityBuilder<'a> {
    let eb = new_entity;

    // Spawn in the specified location
    match pos {
        SpawnType::AtPosition{x,y} => eb.with(Position{ x, y }),
        SpawnType::Carried{by} => eb.with(InBackpack{ owner: by }),
        SpawnType::Equipped{by} => {
            let slot = find_slot_for_equippable_item(tag, raws);
            eb.with(Equipped{ owner: by, slot })
        }
    }
}
}

There's a few notable thins here:

  • We've had to change the method signature, so you're going to have to fix calls to it. It now needs access to the raws files, and the name tag of the item you are spawning.
  • Because we're passing references into it, and EntityBuilder actually contains a reference to the ECS, we had to add some lifetime decorations to tell Rust that the returned EntityBuilder doesn't rely on the tag or the raw files sticking around as a valid reference. So we name a lifetime a - and attach it to the function name (spawn_position<'a> is declaring that 'a is a lifetime it uses). Then we tack on <'a> to the types that share that lifetime. This is enough of a hint to avoid scaring the lifetime checker.
  • We return from a match; AtPosition and Carried are simple; Equipped makes use of the tag finder we just wrote.

We'll have to change three lines to use the new function signature. They are the same; find eb = spawn_position(pos, eb); and replace with eb = spawn_position(pos, eb, key, raws);.

Unfortunately, I ran into another issue while implementing this. We've been passing in a new entity (ecs.create_entity()) to our spawn_named_x functions. Unfortunately, this is going to be a problem: we're starting to need entity spawns to trigger other entity spawns (for example, spawning an NPC with equipment - below - or spawning a chest with contents). Let's fix that now so we don't have issues later.

We'll change the function signature for spawn_named_item, and change the first use of eb to actually create the entity:


#![allow(unused)]
fn main() {
pub fn spawn_named_item(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> {
    if raws.item_index.contains_key(key) {
        let item_template = &raws.raws.items[raws.item_index[key]];

        let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
        ...
}

We'll do the same for spawn_named_mob:


#![allow(unused)]
fn main() {
pub fn spawn_named_mob(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> {
    if raws.mob_index.contains_key(key) {
        let mob_template = &raws.raws.mobs[raws.mob_index[key]];

        let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
        ...
}

And again for spawn_named_prop:


#![allow(unused)]
fn main() {
pub fn spawn_named_prop(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> {
    if raws.prop_index.contains_key(key) {
        let prop_template = &raws.raws.props[raws.prop_index[key]];

        let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
        ...
}

This then requires that we change the signature of spawn_named_entity and what it passes through:


#![allow(unused)]
fn main() {
pub fn spawn_named_entity(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option<Entity> {
    if raws.item_index.contains_key(key) {
        return spawn_named_item(raws, ecs, key, pos);
    } else if raws.mob_index.contains_key(key) {
        return spawn_named_mob(raws, ecs, key, pos);
    } else if raws.prop_index.contains_key(key) {
        return spawn_named_prop(raws, ecs, key, pos);
    }

    None
}
}

In spawner.rs, we need to change the calling signature for spawn_named_entity:


#![allow(unused)]
fn main() {
let spawn_result = spawn_named_entity(&RAWS.lock().unwrap(), ecs, &spawn.1, SpawnType::AtPosition{ x, y});
if spawn_result.is_some() {
    return;
}
}

If you're wondering why we couldn't pass both the ECS and the new entity, it's because of Rust's borrow checker. New entities actually keep hold of a reference to their parent ECS (so when you call build they know the world into which they should be inserted). So if you try and send both &mut World and a new entity - you get errors because you have two "borrows" into the same object. It's probably safe to do that, but Rust can't prove it - so it warns you. This actually prevents an entire class of bug found regularly in the C/C++ world, so while it's a pain - it's for our own good.

So now we can update the player function in spawners.rs to start with a rusty longsword:


#![allow(unused)]
fn main() {
pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
    let mut skills = Skills{ skills: HashMap::new() };
    skills.skills.insert(Skill::Melee, 1);
    skills.skills.insert(Skill::Defense, 1);
    skills.skills.insert(Skill::Magic, 1);

    let player = ecs
        .create_entity()
        .with(Position { x: player_x, y: player_y })
        .with(Renderable {
            glyph: rltk::to_cp437('@'),
            fg: RGB::named(rltk::YELLOW),
            bg: RGB::named(rltk::BLACK),
            render_order: 0
        })
        .with(Player{})
        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
        .with(Name{name: "Player".to_string() })
        .with(HungerClock{ state: HungerState::WellFed, duration: 20 })
        .with(Attributes{
            might: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) },
            fitness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) },
            quickness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) },
            intelligence: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) },
        })
        .with(skills)
        .with(Pools{
            hit_points : Pool{ 
                current: player_hp_at_level(11, 1), 
                max: player_hp_at_level(11, 1) 
            },
            mana: Pool{
                current: mana_at_level(11, 1),
                max: mana_at_level(11, 1)
            },
            xp: 0,
            level: 1
        })
        .marked::<SimpleMarker<SerializeMe>>()
        .build();

    // Starting equipment
    spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Rusty Longsword", SpawnType::Equipped{by : player});

    player
}
}

The main changes here are that we put the new entity into a variable named player, before turning it. We then use that as the parameter for who is holding the "Rusty Longsword" we spawn via spawn_named_entity.

If you cargo run now, you'll start with a rusty longsword. It won't work, but you have it:

Screenshot

Making the rusty longsword do some damage

We left a number of placeholders in the melee_combat_system.rs. Now, it's time to fill in the weapon gaps. Open up the file, and we'll first add some more types we need:


#![allow(unused)]
fn main() {
impl<'a> System<'a> for MeleeCombatSystem {
    #[allow(clippy::type_complexity)]
    type SystemData = ( Entities<'a>,
                        WriteExpect<'a, GameLog>,
                        WriteStorage<'a, WantsToMelee>,
                        ReadStorage<'a, Name>,
                        ReadStorage<'a, Attributes>,
                        ReadStorage<'a, Skills>,
                        WriteStorage<'a, SufferDamage>,
                        WriteExpect<'a, ParticleBuilder>,
                        ReadStorage<'a, Position>,
                        ReadStorage<'a, HungerClock>,
                        ReadStorage<'a, Pools>,
                        WriteExpect<'a, rltk::RandomNumberGenerator>,
                        ReadStorage<'a, Equipped>,
                        ReadStorage<'a, MeleeWeapon>
                      );

    fn run(&mut self, data : Self::SystemData) {
        let (entities, mut log, mut wants_melee, names, attributes, skills, mut inflict_damage, 
            mut particle_builder, positions, hunger_clock, pools, mut rng,
            equipped_items, meleeweapons) = data;
            ...
}

Then we'll add some code to put in the default weapon information, and then search for a replacement if the attacker has something equipped:


#![allow(unused)]
fn main() {
let mut weapon_info = MeleeWeapon{
    attribute : WeaponAttribute::Might,
    hit_bonus : 0,
    damage_n_dice : 1,
    damage_die_type : 4,
    damage_bonus : 0                    
};

for (wielded,melee) in (&equipped_items, &meleeweapons).join() {
    if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee {
        weapon_info = melee.clone();
    }
}
}

That makes substituting in the weapon-dependent parts of the code quite easy:


#![allow(unused)]
fn main() {
let natural_roll = rng.roll_dice(1, 20);
let attribute_hit_bonus = if weapon_info.attribute == WeaponAttribute::Might 
    { attacker_attributes.might.bonus } 
    else { attacker_attributes.quickness.bonus};
let skill_hit_bonus = skill_bonus(Skill::Melee, &*attacker_skills);
let weapon_hit_bonus = weapon_info.hit_bonus;
}

We can also swap in the weapon's damage information:


#![allow(unused)]
fn main() {
let base_damage = rng.roll_dice(weapon_info.damage_n_dice, weapon_info.damage_die_type);
let attr_damage_bonus = attacker_attributes.might.bonus;
let skill_damage_bonus = skill_bonus(Skill::Melee, &*attacker_skills);
let weapon_damage_bonus = weapon_info.damage_bonus;
}

Now, if you cargo run the project you can use your sword to hack the rats into little pieces!

Screenshot

Oh - well that didn't go so well! We were hitting for plenty of damage, but the rats soon overwhelmed us - even doing default damage of 1d4 with a might penalty! As you saw in the recording, I tried to retreat with the intention of healing - and noticed that I was hungry (took too long to find the house!) and couldn't. Fortunately, we have everything we need to also add some food to the player's inventory. The design document states that you should be starting with a stein of beer and a dried sausage. Let's put those into spawns.json:

{
    "name" : "Dried Sausage",
    "renderable": {
        "glyph" : "%",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "consumable" : {
        "effects" : { 
            "food" : ""
        }
    }
},

{
    "name" : "Beer",
    "renderable": {
        "glyph" : "!",
        "fg" : "#FF00FF",
        "bg" : "#000000",
        "order" : 2
    },
    "consumable" : {
        "effects" : { "provides_healing" : "4" }
    }
},

The sausage is a copy of "rations" - and cures hunger. The Beer is a super-weak health potion, but these would have been enough to defeat the rodent menace!

Let's modify player in spawner.rs to also include these in the player's backpack:


#![allow(unused)]
fn main() {
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Dried Sausage", SpawnType::Carried{by : player} );
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Beer", SpawnType::Carried{by : player});
}

Now the player starts with a healing option, and a anti-hunger device (commonly known as food).

But wait - we're naked, with only a sausage and some beer? I didn't think it was THAT sort of game?

Nobody in the game is currently wearing anything. That's probably ok for the rats, but we weren't envisioning a massively liberal society here for humans. More importantly, we also don't have any armor class bonuses if we don't have anything to wear!

In components.rs, we'll replace DefenseBonus with Wearable - and flesh it out a bit. (Don't forget to change the component in main.rs and saveload_system.rs):


#![allow(unused)]
fn main() {
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct Wearable {
    pub armor_class : f32
}
}

That's a very simple change. Let's update our raw file reader in raws/item_structs.rs to reflect what we want:

{
    "name" : "Shield",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00AAFF",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Shield",
        "armor_class" : 1.0
    }
},

We probably also want to support more equipment slots! In components.rs, we should update EquipmentSlot to handle more possible locations:


#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum EquipmentSlot { Melee, Shield, Head, Torso, Legs, Feet, Hands }
}

We'll undoubtedly add more later, but that covers the basics. We need to update raws/item_structs.rs to reflect the changes:


#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug)]
pub struct Item {
    pub name : String,
    pub renderable : Option<Renderable>,
    pub consumable : Option<Consumable>,
    pub weapon : Option<Weapon>,
    pub wearable : Option<Wearable>
}
...
#[derive(Deserialize, Debug)]
pub struct Wearable {
    pub armor_class: f32,
    pub slot : String
}
}

We're going to need to convert from strings (in JSON) to EquipmentSlot a few times, so we'll add a function to raws/rawmaster.rs to do this:


#![allow(unused)]
fn main() {
pub fn string_to_slot(slot : &str) -> EquipmentSlot {
    match slot {
        "Shield" => EquipmentSlot::Shield, 
        "Head" => EquipmentSlot::Head,
        "Torso" => EquipmentSlot::Torso, 
        "Legs" => EquipmentSlot::Legs, 
        "Feet" => EquipmentSlot::Feet, 
        "Hands" => EquipmentSlot::Hands,
        "Melee" => EquipmentSlot::Melee,
        _ => { rltk::console::log(format!("Warning: unknown equipment slot type [{}])", slot)); EquipmentSlot::Melee }
    }
}
}

We'll want to expand our spawn_named_item code in raws/rawmaster.rs to handle the expanded options:


#![allow(unused)]
fn main() {
if let Some(wearable) = &item_template.wearable {
    let slot = string_to_slot(&wearable.slot);
    eb = eb.with(Equippable{ slot });
    eb = eb.with(Wearable{ slot, armor_class: wearable.armor_class });
}
}

Let's make a few more items in spawns.json to give the player something to wear:

{
    "name" : "Stained Tunic",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Torso",
        "armor_class" : 0.1
    }
},

{
    "name" : "Torn Trousers",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FFFF",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Legs",
        "armor_class" : 0.1
    }
},

{
    "name" : "Old Boots",
    "renderable": {
        "glyph" : "[",
        "fg" : "#FF9999",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Legs",
        "armor_class" : 0.1
    }
}

Now we'll open up spawner.rs and add these items to the player:


#![allow(unused)]
fn main() {
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Rusty Longsword", SpawnType::Equipped{by : player});
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Dried Sausage", SpawnType::Carried{by : player} );
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Beer", SpawnType::Carried{by : player});
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Stained Tunic", SpawnType::Equipped{by : player});
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Torn Trousers", SpawnType::Equipped{by : player});
spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Old Boots", SpawnType::Equipped{by : player});
}

If you run the game now, checking your inventory and removing items will show you that you are starting correctly - with your stained shirt, torn trousers, old boots, beer, sausage and a rusty sword.

We still have one more thing to do with wearables; melee_system.rs needs to know how to calculate armor class. Fortunately, this is quite easy:


#![allow(unused)]
fn main() {
let mut armor_item_bonus_f = 0.0;
for (wielded,armor) in (&equipped_items, &wearables).join() {
    if wielded.owner == wants_melee.target {
        armor_item_bonus_f += armor.armor_class;
    }
}
let base_armor_class = 10;
let armor_quickness_bonus = target_attributes.quickness.bonus;
let armor_skill_bonus = skill_bonus(Skill::Defense, &*target_skills);
let armor_item_bonus = armor_item_bonus_f as i32;
}

We iterate through equipped armor, and add the bonus together for each item that is worn by the defender. Then we truncate the number down to an integer.

So why are we using floating point numbers? Classic D&D assigns armor values to complete sets of armor. So in 5th Edition, leather armor has an AC of 11 (plus dexterity). In our game, you can wear the pieces of leather armor separately - so we give them a value equivalent to part of the desired AC for a full set. Then we add them together, to handle piecemeal armor (you found a nice breastplate and only leather leggings, for example).

Ok, so I'm wearing clothes - why isn't everyone else?

We've got far enough in implementing clothing and weaponry that we can start to give it to NPCs. Since the Barkeep is our favorite test victim, lets decorate his entry with how we'd like to spawn items:

{
    "name" : "Barkeep",
    "renderable": {
        "glyph" : "☺",
        "fg" : "#EE82EE",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 4,
    "ai" : "vendor",
    "attributes" : {
        "intelligence" : 13
    },
    "skills" : {
        "Melee" : 2
    },
    "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ]
},

Easy enough: we've added an array called equipped, and list out everything we'd like the barkeep to wear. Of course, we now have to write those items.

{
    "name" : "Cudgel",
    "renderable": {
        "glyph" : "/",
        "fg" : "#A52A2A",
        "bg" : "#000000",
        "order" : 2
    },
    "weapon" : {
        "range" : "melee",
        "attribute" : "Quickness",
        "base_damage" : "1d4",
        "hit_bonus" : 0
    }
},

{
    "name" : "Cloth Tunic",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Torso",
        "armor_class" : 0.1
    }
},

{
    "name" : "Cloth Pants",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FFFF",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Legs",
        "armor_class" : 0.1
    }
},

{
    "name" : "Slippers",
    "renderable": {
        "glyph" : "[",
        "fg" : "#FF9999",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Legs",
        "armor_class" : 0.1
    }
}

There's nothing new there, just data-entry. We need to modify our raws/mob_structs.rs file to accommodate giving NPCs equipment:


#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug)]
pub struct Mob {
    pub name : String,
    pub renderable : Option<Renderable>,
    pub blocks_tile : bool,
    pub vision_range : i32,
    pub ai : String,
    pub quips : Option<Vec<String>>,
    pub attributes : MobAttributes,
    pub skills : Option<HashMap<String, i32>>,
    pub level : Option<i32>,
    pub hp : Option<i32>,
    pub mana : Option<i32>,
    pub equipped : Option<Vec<String>>
}
}

Again, easy enough - we optionally provide a list of strings (representing item name tags) to the mob. So we have to modify spawn_named_mob in raws/rawmaster.rs to handle this. We'll replace Some(eb.build()) with:


#![allow(unused)]
fn main() {
let new_mob = eb.build();

// Are they wielding anyting?
if let Some(wielding) = &mob_template.equipped {
    for tag in wielding.iter() {
        spawn_named_entity(raws, ecs, tag, SpawnType::Equipped{ by: new_mob });
    }
}

return Some(new_mob);
}

This makes the new mob, and stores the entity as new_mob. It then looks to see if there's a an equipped field in the mob template; if there is, it iterates through it, spawning each item as equipped on the mob.

If you cargo run now, you'll find that you are wearing clothing, wielding a rusty sword, and have your beer and sausage.

Screenshot

Dressing up your NPCs

I won't paste in the full spawns.json file in here, but if you check the source you'll find that I've added clothing to our basic NPCs. For now, they are all the same as the barkeep - hopefully we'll remember to adjust these in the future!

What about natural attacks and defenses?

So the NPCs are nicely dressed and equipped, which gives them combat stats and armor classes. But what about our rats? Rats don't typically wear cute little rat suits and carry weaponry (if you have rats that do: start running). Instead, they have natural attacks and defenses. They typically bite to attack (we're not going to worry about rat-borne disease just yet; everyone dying of Black Plague would be sad), and rely on their natural fur coat to provide a minimal level of protection.

Some creatures have more than one natural attack. Ignoring breath weapon and tails, Dragons are often listed as having "claw, claw, bite" (for the front claws and their mouth). Even kittens can claw and bite. So we need to support multiple natural attacks from a creature. Let's decorate our Rat to show how we'd like to handle this:

{
    "name" : "Rat",
    "renderable": {
        "glyph" : "r",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 8,
    "ai" : "melee",
    "attributes" : {
        "Might" : 3,
        "Fitness" : 3
    },
    "skills" : {
        "Melee" : -1,
        "Defense" : -1
    },
    "natural" : {
        "armor_class" : 11,
        "attacks" : [
            { "name" : "bite", "hit_bonus" : 0, "damage" : "1d4" }
        ]   
    }
},

To support this, we'll have to add to raws/mob_structs.rs:


#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug)]
pub struct Mob {
    pub name : String,
    pub renderable : Option<Renderable>,
    pub blocks_tile : bool,
    pub vision_range : i32,
    pub ai : String,
    pub quips : Option<Vec<String>>,
    pub attributes : MobAttributes,
    pub skills : Option<HashMap<String, i32>>,
    pub level : Option<i32>,
    pub hp : Option<i32>,
    pub mana : Option<i32>,
    pub equipped : Option<Vec<String>>,
    pub natural : Option<MobNatural>
}
...
#[derive(Deserialize, Debug)]
pub struct MobNatural {
    pub armor_class : Option<i32>,
    pub attacks: Option<Vec<NaturalAttack>>
}

#[derive(Deserialize, Debug)]
pub struct NaturalAttack {
    pub name : String,
    pub hit_bonus : i32,
    pub damage : String
}
}

This is much like the raw file reading we've done before: it optionally loads a natural key, and fills out the Mob's natural armor and attacks. Now we have to make use of this information. This will, of course, require even more components! In components.rs, add the following (and don't forget to register them in main.rs and saveload_system.rs; you only have to register NaturalAttackDefense - the other structure isn't a component, just part of one):


#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize, Clone)]
pub struct NaturalAttack {
    pub name : String,
    pub damage_n_dice : i32,
    pub damage_die_type : i32,
    pub damage_bonus : i32,
    pub hit_bonus : i32
}

#[derive(Component, Serialize, Deserialize, Clone)]
pub struct NaturalAttackDefense {
    pub armor_class : Option<i32>,
    pub attacks : Vec<NaturalAttack>
}
}

In turn, we have to adjust raws/rawmaster.rs's spawn_named_mob function to generate this data:


#![allow(unused)]
fn main() {
if let Some(na) = &mob_template.natural {
    let mut nature = NaturalAttackDefense{
        armor_class : na.armor_class,
        attacks: Vec::new()
    };
    if let Some(attacks) = &na.attacks {
        for nattack in attacks.iter() {
            let (n, d, b) = parse_dice_string(&nattack.damage);
            let attack = NaturalAttack{
                name : nattack.name.clone(),
                hit_bonus : nattack.hit_bonus,
                damage_n_dice : n,
                damage_die_type : d,
                damage_bonus: b
            };
            nature.attacks.push(attack);
        }
    }
    eb = eb.with(nature);
}
}

Finally, we need to adjust melee_combat_system.rs to use this data. We'll start by giving the system the ability to read from our new NaturalAttackDefense store:


#![allow(unused)]
fn main() {
...
                        ReadStorage<'a, NaturalAttackDefense>
                      );

    fn run(&mut self, data : Self::SystemData) {
        let (entities, mut log, mut wants_melee, names, attributes, skills, mut inflict_damage, 
            mut particle_builder, positions, hunger_clock, pools, mut rng,
            equipped_items, meleeweapons, wearables, natural) = data;
...
}

Armor class is relatively easy; we'll adjust our base_armor_class to not be a constant:


#![allow(unused)]
fn main() {
let base_armor_class = match natural.get(wants_melee.target) {
    None => 10,
    Some(nat) => nat.armor_class.unwrap_or(10)
};
}

That's a bit of a mouthful! So we get the natural attacks info, and if its None - we stick to using 10 as our base. Then we use unwrap_or to either use the natural armor class (if there is one), or 10 (if there isn't). So why are we modifying the base? This allows you to have a beastie with natural armor who also wears actual armor. For example, you might have a demon lord who gets some natural armor just for being a cool demon, and he also gets to benefit from the cool looking plate and mail you gave him.

Natural attacks are a bit more complicated:


#![allow(unused)]
fn main() {
let mut weapon_info = MeleeWeapon{
    attribute : WeaponAttribute::Might,
    hit_bonus : 0,
    damage_n_dice : 1,
    damage_die_type : 4,
    damage_bonus : 0                    
};

if let Some(nat) = natural.get(entity) {
    if !nat.attacks.is_empty() {
        let attack_index = if nat.attacks.len()==1 { 0 } else { rng.roll_dice(1, nat.attacks.len() as i32) as usize -1 };
        weapon_info.hit_bonus = nat.attacks[attack_index].hit_bonus;
        weapon_info.damage_n_dice = nat.attacks[attack_index].damage_n_dice;
        weapon_info.damage_die_type = nat.attacks[attack_index].damage_die_type;
        weapon_info.damage_bonus = nat.attacks[attack_index].damage_bonus;
    }
}
}

So we keep the weapon_info as before, and then we look to see if the entity has a NaturalAttackDefense attribute. If it does, we check that there are any natural attacks. If there are, we pick one at random - and turn the weapon info into that attack type. This prevents monsters from completely shredding the player with multiple attacks at once, but allows you to spread out the attack types.

If you cargo run now, the rats have slightly more natural behavior. They may well still kill the beginning adventurer, but that's roguelike life!

Wrap-Up

In this chapter, we've gained a lot of functionality:

  • We've implemented a simplified d20 system to simulate attributes and combat.
  • We've given all the entities in the game stats.
  • We've given entities equipment, and allowed us to specify their load-out in the raw files.
  • Monsters can now have natural attacks.

In other words: we've come a long way in a relatively long chapter! The great news is that we now have a very solid base on which to build a real game.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)

Copyright (C) 2019, Herbert Wolverson.