~/adam.log

Hands on Rust - 15 - Combat Systems and Loot

Published 2026-08-11

15. Combat Systems and Loot

We have done a lot so far with building function to build and place all the items within our world. This has made it so we can test new things and make sure that they exist. But this is not the normal way of doing things. We want to be able to create as many items as we want and then have them appear throughout the dungeon based off of level and quantity desired. This is where data-driven design comes in.


This is be a file that we write all the entities that we want and then have the program pull from that file to populate the dungeon.




Designing Data-Driven Dungeons

For this first step we want to create a new file resources/template.ron (Rusty Object Notation)that we will house the items that we want to have in our dungeons. We know that they all will share something in common. but they will have differing values and even different components. Start by creating the file and adding the following:

Templates(
    entities : [
        Template(
                entity_type: Item,
                name : "Healing Potion", glyph : '!', levels : [ 0, 1, 2 ],
                provides: Some([ ("Healing", 6) ]),
                frequency: 2
        ),
            Template(
                entity_type: Item,
                name : "Dungeon Map", glyph : '{', levels : [ 0, 1, 2 ],
                provides: Some([ ("MagicMap", 0) ]),
                frequency: 1
        ),
            Template(
                entity_type: Enemy,
                name : "Goblin", glyph : 'g', levels : [ 0, 1, 2 ],
                hp : Some(1),
                frequency: 3
        ),
            Template(
                entity_type: Enemy,
                name : "Orc", glyph : 'o', levels : [ 0, 1, 2 ],
                hp : Some(2),
                frequency: 2
        ),
    ],
)

This format should be familiar to you as its very similar to a JSON data type. There is now frequency and provides for some of the entries. Here is a little break down of all the entires.

• entity_type is either Item or Enemy.
• name is the entity’s display name.
• glyph defines the character used to render the entity.
• provides is either not present or wrapped in an Option. This property is a list
    of effects the item provides, such as Healing or MagicMap. A second
    number, if provided in the tuple, indicates how much of the listed effect
    the item or enemy applies, if applicable (for example, healing potions heal
    for 6 hit points).
• hp is also an Option because not every item or enemy has hit points.

We also have two new fields:
• levels is a list of the levels on which the entity can spawn, starting at zero.
• frequency is an indicator for how often the item spawns; the higher the
    number, the more often it spawns.

One thing to keep in mind while building your own game is that you will need to work on the data type (format) and then the data itself. You can build either first but no matter what you do if you change one then other must follow.


Reading Dungeon Data

You now have a file that contains the data for spawns. We will need a way to put that data into ram. We will use the Serde crate to do this. As with any new create we need to go to the dependence part of the cargo.toml. We will also need a format for Serde to recognize so we will add Ron as well.

[dependencies]
bracket-lib = "~0.8.1"
legion = "=0.3.1"
serde = { version = "=1.0.115" }
ron = "=0.6.1"

Extending the Spawner Module

Now we need to turn the single file spawners.rs into a multi-file module. Let’s take these steps.

1. Create a new folder named spawner.
2. Move spawner.rs into the new directory.
3. Rename spawner.rs to mod.rs.

Mapping and Loading the Template

Okay so we need to create a new file to the spawner folder. Create the file spawner/template.rs and then be sure to add that to the mod.rs file we just renamed.

// mod.rs
mod template;

// template.rs
use crate::prelude::*;
use legion::systems::CommandBuffer;
use ron::de::from_reader;
use serde::Deserialize;
use std::collections::HashSet;
use std::fs::File;

#[derive(Clone, Deserialize, Debug)]
pub struct Template {
    pub entity_type: EntityType,
    pub levels: HashSet<usize>,
    pub frequency: i32,
    pub name: String,
    pub glyph: char,
    pub provides: Option<Vec<(String, i32)>>,
    pub hp: Option<i32>,
}

#[derive(Clone, Deserialize, Debug, PartialEq)]
pub enum EntityType {
    Enemy,
    Item,
}

#[derive(Clone, Deserialize, Debug)]
pub struct Templates {
    pub entities: Vec<Template>,
}

Okay to go over this. serde::deserialize allows you add structs and enums for files, ron::de::from_reader allows you to deserialize RON from files, std::fs::File is similar to std::io, the rest is setting up the structs that we will be pulling from the template.ron file.


Now we need to actually open up the file from the system. Let’s add in that function now.

impl Templates {
    pub fn load() -> Self {
        let file = File::open("resources/template.ron").expect("Failed opening file");
        from_reader(file).expect("Unable to load templates")
    }
}

We need to deal with the problem of it not having access to the file or it not existing, so that is the first line of the function, the other line from_reader is there in-case serde can’t read the file.


Data-Driven Spawning

Okay so every item within our world will start as a data template and then be spawned into the world. We want to be able to pull all the entities from the data sheet and the build from there. We want to now use that Template that we have and have it spawn_entities let’s start with that.

impl Templates {
    ...
    pub fn spawn_entities(
        &self,
        ecs: &mut World,
        rng: &mut RandomNumberGenerator,
        level: usize,
        spawn_points: &[Point],
    ) {
        let mut available_entities = Vec::new();
        self.entities
            .iter()
            .filter(|e| e.levels.contains(&level))
            .for_each(|t| {
                for _ in 0..t.frequency {
                    available_entities.push(t);
                }
            });

        let mut commands = CommandBuffer::new(ecs);
        spawn_points.iter().for_each(|pt| {
            if let Some(entity) = rng.random_slice_entry(&available_entities) {
                self.spawn_entity(pt, entity, &mut commands);
            }
        });
        commands.flush(ecs);
    }
}

You will create the spawn_entities function that will have all the needed muts and self, for it you will create a vector that we will fill with the entities we want to spawn, filter the entities that come from the template file, filter for the level that we are spawning, then for the size of the frequency we will add in the entity to be placed. Lastly we need to find the right place to put the entity.


Okay so that is how we will spawn all the entities for the dungeon but now we need one for just a single entity.

impl Templates {
    ...
    fn spawn_entity(
        &self,
        pt: &Point,
        template: &Template,
        commands: &mut legion::systems::CommandBuffer,
    ) {
        let entity = commands.push((
            pt.clone(),
            Render {
                color: ColorPair::new(WHITE, BLACK),
                glyph: to_cp437(template.glyph),
            },
            Name(template.name.clone()),
        ));
}
}

You will create the new entity with the push function, clone the pt, use the field from glyph, clone the name template as you don’t want to pull the data right from the file. Now we can start to add the enemies and items to the world.

match template.entity_type {
    EntityType::Item => commands.add_component(entity, Item {}),
    EntityType::Enemy => {
        commands.add_component(entity, Enemy {});
        commands.add_component(entity, FieldOfView::new(6));
        commands.add_component(entity, ChasingPlayer {});
        commands.add_component(
            entity,
            Health {
                current: template.hp.unwrap(),
                max: template.hp.unwrap(),
            },
        );
    }
}

Lastly we need to take care of the instance were there is an effect that will or won’t need to be added into the entity.

if let Some(effects) = &template.provides {
    effects
        .iter()
        .for_each(|(provides, n)| match provides.as_str() {
            "Healing" => commands.add_component(entity, ProvidesHealing { amount: *n }),
            "MagicMap" => commands.add_component(entity, ProvidesDungeonMap {}),
            _ => {
                println!("Warning: we don't know how to provide {}", provides);
            }
        });
}

One thing to note here is that we added the items as a list so that we can add more than one effect when we use an item.


Spring Cleaning

We just turned many function into a few so we need to delete the following from the spawner/mod.rs:

• spawn_entity
• spawn_monster
• goblin
• orc
• spawn_healing_potion
• spawn_magic_mapper

With that out of the way we need to start to build all the needed functions to build the entire level. We will use a spawn_level function within the mod.rs to take the data file and build what we need. Make sure to add the use template::Template; to the top of the module.

// spawner/mod.rs
pub fn spawn_level(
    ecs: &mut World,
    rng: &mut RandomNumberGenerator,
    level: usize,
    spawn_points: &[Point],
) {
    let template = Templates::load();
    template.spawn_entities(ecs, rng, level, spawn_points)
}

// we need to replace the spawn_entity with spawn_level
// main.rs
// replace
map_builder.monster_spawns
    .iter()
    .for_each(|pos| spawn_entity(&mut ecs, &mut rng, *pos));
// with
spawn_level(
    &mut self.ecs, // only &mut ecs for the new()
    &mut rng,
    0,
    &map_builder.monster_spawns
);
// advance level needs to pass the current level
spawn_level(
    &mut self.ecs,
    &mut rng,
    map_level as usize,
    &map_builder.monster_spawns,
);

You should be able to run the game now. Once you know it works feel free to change the values for items and then add this item to the template.

Template(
    entity_type: Item,
    name : "Weak Healing Potion", glyph : '!', levels : [ 0, 1, 2 ],
    provides: Some([ ("Healing", 2) ]),
    frequency: 2
),
Template(
    entity_type: Enemy,
    name : "Armored Goblin", glyph : 'm', levels : [ 1, 2 ],
    hp : Some(2),
    frequency: 1
),
Template(
    entity_type: Enemy,
    name : "Orc Commander", glyph : 'e', levels : [ 2 ],
    hp : Some(4),
    frequency: 1
),

Feel free to add more and more enemies and items to the world you will need to be sure that you have a glyph for them and then the rest of the data.




Extending the Combat System

Right now we have a very basic combat system where we all do a single point of damage. We can change that as well as add the ability to upgrade our weapons.


Damage from Weapons and Claws

Open up the spawner/template.rs and add the following field to the struct.

#[derive(Clone, Deserialize, Debug)] // (4)
pub struct Template {
    // (5)
    pub entity_type: EntityType, // (6)
    pub levels: HashSet<usize>,  // (7)
    pub frequency: i32,
    pub name: String,
    pub glyph: char,
    pub provides: Option<Vec<(String, i32)>>, // (8)
    pub hp: Option<i32>,
    pub base_damage: Option<i32>,
}

Now we need to update all the entries within the template.ron so that we have that field.

Templates(
    entities : [
        Template(
                entity_type: Item,
                name : "Healing Potion", glyph : '!', levels : [ 0, 1, 2 ],
                provides: Some([ ("Healing", 6) ]),
                frequency: 2
        ),
        Template(
            entity_type: Item,
            name : "Dungeon Map", glyph : '{', levels : [ 0, 1, 2 ],
            provides: Some([ ("MagicMap", 0) ]),
            frequency: 1
        ),
        Template(
            entity_type: Item,
            name : "Weak Healing Potion", glyph : '!', levels : [ 0, 1, 2 ],
            provides: Some([ ("Healing", 2) ]),
            frequency: 2
        ),
        Template(
            entity_type: Enemy,
            name : "Goblin", glyph : 'g', levels : [ 0, 1, 2 ],
            hp : Some(1),
            frequency: 3,
            base_damage: Some(1)
        ),
        Template(
            entity_type: Enemy,
            name : "Orc", glyph : 'o', levels : [ 0, 1, 2 ],
            hp : Some(2),
            frequency: 2,
            base_damage: Some(1)
        ),
        Template(
            entity_type: Enemy,
            name : "Armored Goblin", glyph : 'm', levels : [ 1, 2 ],
            hp : Some(2),
            frequency: 1,
            base_damage: Some(2)
        ),
        Template(
            entity_type: Enemy,
            name : "Ogre", glyph : 'O', levels : [ 1, 2 ],
            hp : Some(5),
            frequency: 1,
            base_damage: Some(2)
        ),
        Template(
            entity_type: Enemy,
            name : "Ettin", glyph : 'E', levels : [ 2 ],
            hp : Some(4),
            frequency: 1,
            base_damage: Some(3)
        ),
    ],
)

Again you should see some new entries. but we now have their base damage. Now we can also add in the “Rusty Sword” that will allow the player to upgrade their damage.

Template(
    entity_type: Item,
    name : "Rusty Sword", glyph: '/', levels: [ 0, 1, 2 ],
    frequency: 1,
    base_damage: Some(1)
),

Now we need a component that will store the damage for the entity.


Damage Component

Open up components.rs and add the new component.

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Damage(pub i32);

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Weapon;

So we have a way to adding a damage to an entity and the component to match, we also have a way of defining a weapon that isn’t a player, enemy, or item.


Let’s head to the spawn_player within the mod.rs

pub fn spawn_player(ecs: &mut World, pos: Point) {
    ecs.push((
        Player { map_level: 0 },
        pos,
        Render {
            color: ColorPair::new(WHITE, BLACK),
            glyph: to_cp437('@'),
        },
        Health {
            current: 10,
            max: 10,
        },
        FieldOfView::new(8),
        Damage(1),
    ));
}

We now have added in the damage to the player. We also need to add that same thing to the spawn_entity as well though it will be a little different.

if let Some(damage) = &template.base_damage {
    commands.add_component(entity, Damage(*damage));
    if template.entity_type == EntityType::Item {
        commands.add_component(entity, Weapon {});
    }
}

We will only add this if the entity has a damage but otherwise it will not do anything.


Doing some Damage

Now we need to leverage the new component and maybe the fact that the player has a new weapon. Head to the systems/combat.rs and let’s add that in.

use crate::prelude::*;
#[system]
#[read_component(WantsToAttack)]
#[read_component(Player)]
#[write_component(Health)]
#[read_component(Damage)]
#[read_component(Carried)]
pub fn combat(ecs: &mut SubWorld, commands: &mut CommandBuffer) {
    let mut attackers = <(Entity, &WantsToAttack)>::query();
    let victims: Vec<(Entity, Entity, Entity)> = attackers
        .iter(ecs)
        .map(|(entity, attack)| (*entity, attack.attacker, attack.victim))
        .collect();
    victims.iter().for_each(|(message, attacker, victim)| {
        let is_player = ecs
            .entry_ref(*victim)
            .unwrap()
            .get_component::<Player>()
            .is_ok();

        let base_damage = if let Ok(v) = ecs.entry_ref(*attacker) {
            if let Ok(dmg) = v.get_component::<Damage>() {
                dmg.0
            } else {
                0
            }
        } else {
            0
        };
        let weapon_damage: i32 = <(&Carried, &Damage)>::query()
            .iter(ecs)
            .filter(|(carried, _)| carried.0 == *attacker)
            .map(|(_, dmg)| dmg.0)
            .sum();
        let final_damage = base_damage + weapon_damage;
        if let Ok(mut health) = ecs
            .entry_mut(*victim)
            .unwrap()
            .get_component_mut::<Health>()
        {
            health.current -= final_damage;
            if health.current < 1 && !is_player {
                commands.remove(*victim);
            }
        }
        commands.remove(*message);
    });
}

There are a few changes here, we added in the new reads, we added in the attack to the victims, once we had that we made sure to check the base damage, then the weapon carried by the attacker, then health of the victim, then remove the final damage.


The Adventurer Isn’t and Octopus

We now need to be sure that the adventurer can only carry a single weapon and gets rid of worse weapons if they pick up a better one. Head to systems/player_input.rs add in the read so that the player can see the Weapon and change the code.

VirtualKeyCode::G => {
    let (player, player_pos) = players
        .iter(ecs)
        .find_map(|(entity, pos)| Some((*entity, *pos)))
        .unwrap();
    let mut items = <(Entity, &Item, &Point)>::query();
    items
        .iter(ecs)
        .filter(|(_entity, _item, &item_pos)| item_pos == player_pos)
        .for_each(|(entity, _item, _item_pos)| {
            commands.remove_component::<Point>(*entity);
            commands.add_component(*entity, Carried(player));
            if let Ok(e) = ecs.entry_ref(*entity) {
                if e.get_component::<Weapon>().is_ok() {
                    <(Entity, &Carried, &Weapon)>::query()
                        .iter(ecs)
                        .filter(|(_, c, _)| c.0 == player)
                        .for_each(|(e, c, w)| {
                            commands.remove(*e);
                        })
                }
            }
        });
    Point::new(0, 0)
}

We check to see if it’s a weapon and then check against the weapon already carried.




Adding More Swords

Let’s add some more swords and call it a day as we want to see some really cool swords for the player.

// template.ron
Template(
    entity_type: Item,
    name : "Rusty Sword", glyph: 's', levels: [ 0, 1, 2 ],
    frequency: 1,
    base_damage: Some(1)
),
Template(
    entity_type: Item,
    name : "Shiny Sword", glyph: 'S', levels: [ 0, 1, 2 ],
    frequency: 1,
    base_damage: Some(2)
),
Template(
    entity_type: Item,
    name : "Huge Sword", glyph: '/', levels: [ 1, 2 ],
    frequency: 1,
    base_damage: Some(3)
),



Wrap-Up

Well you did it you are almost completely done with the game. You have a template that you can use to describe all the items that you want as well as enemies and more. You can set the levels in which the show-up. Feel free to mess with the template.ron and see if you can make the game more interesting.