Hit Points

In preparation for getting combat working, we need a way to track if entities are injured/dead. The traditional approach in most RPGs and roguelikes is the venerable hit point---run out of hit points, and you die.

Health Component

The first thing we need is a component to represent current health status. We need to track both the maximum and the current level of hit points---that way, if we add healing later we know the ceiling at which to cap improvements. We can also use it as a range when displaying health bars.

Create a new file named src/components/health.rs. Add the new component to it:


#![allow(unused)]
fn main() {
pub struct Health {
    pub max: i32,
    pub current: i32
}
}

The component file won't do anything until we include it in the project. Open src/components/mod.rs and add the two lines highlighted with plus symbols:


#![allow(unused)]
fn main() {
mod tags;
mod targeting;
mod tile_trigger;
+mod health;

pub use colonist::*;
pub use colonist_status::*;
pub use speech::*;
pub use tags::*;
pub use targeting::*;
pub use tile_trigger::*;
+pub use health::*;
}

The health component is now part of the component system, so it's time to start giving entities some hit points.

Giving the Player Some Health

Open src/main.rs and find the code that sets up the player entity. At the end of the component push, add the following:


#![allow(unused)]
fn main() {
Health{max: 10, current: 10},
}

The player now has a health component.

Giving Colonists Health

We also want colonists to have some health. Open src/map/layerbuilder/colonists.rs. Now we run into a problem. Legion's push command doesn't make it easy to add a large number of components at once, so I had to work around it by using commands to add more to the basic entity. The following code block shows you where to add the new code (marked with + symbols):


#![allow(unused)]
fn main() {
fn build_base_colonist(ecs: &mut World, location: Point, layer: u32) -> Entity {
    let name_lock = NAMES.lock();
    let name = name_lock.unwrap().random_human_name();
+   let entity = ecs.push((
        ...
    )); // Add here

+   let mut rng = RandomNumberGenerator::new();
+   let hp = rng.roll_dice(1, 6) + 3;
+   let mut commands = CommandBuffer::new(ecs);
+   commands.add_component(entity, Health{max: hp, current: hp});
+   commands.flush(ecs);

    entity
}
}

All base colonists now have a random number of hit points. About this time, I took a quick detour and decided to change the description of the first colonist. Since we're in the file already, let's do that. Find the spawn_first_colonist function, and add a new description.


#![allow(unused)]
fn main() {
commands.add_component(entity, Description("Colonist senior manager.".to_string()));
commands.flush(ecs);
}
}

Now that colonists have hit points, let's give some to the monsters too.

Monstrous Health

Monsters are created in src/map/layerbuilder/monsters.rs, so open that file. In the list of components given to face eaters (spawn_face_eater function) add:


#![allow(unused)]
fn main() {
Health{max: 3, current: 3},
}

Now that monsters, colonists and the player all have hit points---we need to update the user interface to show this.

UI Updates

We're going to be adding more lines to the UI panel, so first we need to make a quick change to colonist_panel. Adjust the function signature to take mut y: i32 as a parameter, and remove let mut y=2 from the function. Open src/render/colonist_panel.rs and make the following changes:


#![allow(unused)]
fn main() {
pub fn render_colonist_panel(ctx: &mut BTerm, ecs: &World, current_layer: usize, mut y: i32) -> i32 {
    ..
    // let mut y = 2;
}

A New Status Panel

We want to show SecBot's hit points, and not look too incredibly ugly doing it. Open src/render/mod.rs and add two new lines:


#![allow(unused)]
fn main() {
pub mod status_panel;
pub use status_panel::*;
}

Now that we've told Rust to use our new file, create a new file named src/render/status_panel.rs.


#![allow(unused)]
fn main() {
use bracket_lib::prelude::*;
use legion::*;
use crate::components::*;
use crate::map::WIDTH;

pub fn render_status(ctx: &mut BTerm, ecs: &World, mut y: i32) -> i32 {
    let x = WIDTH + 3;
    let mut hp_query = <(&Player, &Health)>::query();
    hp_query.for_each(ecs, |(_, hp)| {
        ctx.print_color(x, y, WHITE, BLACK, format!{"Hit Points        : {} / {}", hp.current, hp.max});
        y += 1;
    });
    ctx.print_color(x, y, GREY, BLACK, "----------------------------");
    y += 1;
    y
}
}

This is pretty straightforward: we display the player's health and a separator, and update the panel's y position.

Tooltips that Show Health

Now open src/render/tooltips.rs. We're going to add a health display to the tooltips. At the top, change the components import to components::*. It makes life easier, even if it slightly slower to compile. Then find the let map_y = my - 1 line, and adjust the entity details query a bit:


#![allow(unused)]
fn main() {
 let map_y = my - 1;
if map_x >= 0 && map_x < WIDTH as i32 && map_y >= 0 && map_y < HEIGHT as i32 {
    let mut lines = Vec::new();
    let mut query = <(Entity, &Position, &Description, &Name)>::query();
    query.for_each(ecs, |(entity, pos, desc, name)| {
        if pos.layer == map.current_layer as u32 && pos.pt.x == map_x && pos.pt.y == map_y {
            let idx = map.get_current().point2d_to_index(pos.pt);
            if map.get_current().visible[idx] {
                lines.push((CYAN, name.0.clone()));
                lines.push((GRAY, desc.0.clone()));
                if let Ok(er) = ecs.entry_ref(*entity) {
                    if let Ok(hp) = er.get_component::<Health>() {
                        lines.push((GRAY, format!("{}/{} hp", hp.current, hp.max)));
                    }
                }
            }
        }
    });
}

See how we now retrieve the Entity as part of the query? Not everything has health, so we don't just require a Health component. Then we check to see if the entity has a Health component, and print the hit points details if they are present.

We've slightly changed the format of the line information. It's now a tuple, containing color and text---rather than just the text. We need to adjust the width calculation for each line:


#![allow(unused)]
fn main() {
        if !lines.is_empty() {
            let height = lines.len() + 1;
            let width = lines.iter().map(|s| s.1.len()).max().unwrap() + 2;
            let tip_x = if map_x < WIDTH as i32 / 2 {
                mx + 1
            } else {
}

Finally, we tweak the render code a little:


#![allow(unused)]
fn main() {
            } else {
                my
            };
            ctx.draw_box(tip_x, tip_y- (lines.len()/2) as i32, width, height, WHITE, BLACK);
            let mut y = tip_y + 1 - (lines.len()/2) as i32;
            lines.iter().for_each(|s| {
                ctx.print_color(tip_x + 1, y, s.0, BLACK, &s.1);
                y += 1;
            });
        }
}

Calling the new code

In src/main.rs, we need to adjust our code that calls the UI rendering to make use of the new y calculation:


#![allow(unused)]
fn main() {
fn tick(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        render::render_ui_skeleton(ctx);
+       let y = render::render_status(ctx, &self.ecs, 2);
+       let y = render::render_colonist_panel(ctx, &self.ecs, self.map.current_layer, y);
        let (_y, target_pt) =
            render::render_targeting_panel(y, ctx, &self.ecs, self.map.current_layer);
        self.map.render(ctx);
}

Wrap-Up

If you play the game now, colonists have hit points and nicer looking tooltips---and SecBot's health is visible in the status panel.

You can find the source code for hit_points here.

Next up: shooting things!