We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
8. Health and Melee Combat
Let’s keep going we are going to add Health Points and a Heads-Up Display that will show the current health points of your Player, We will add a system for the Player “attack” interaction and a Component for the Player Health that an Entity can have. Also bare in mind that you will need to have a system for combat as well.
Giving Entities Hit Points
Okay so let’s start with the component for the Health, head to components.rs and add a new struct.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Health {
pub current: i32,
pub max: i32
}
Adding Health to the Player
Now we can add that to the spawner.rs to ensure that the player will have the component when it is spawned.
pub fn spawn_player(ecs: &mut World, pos: Point) {
ecs.push((
Player,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph: to_cp437('@'),
},
Health {
current: 20,
max: 20,
},
));
}
Boom we now have a Player with 20 Hit Points. Now we need to add a way to see the current health of the Player.
Adding a Heads-up Display
We want to add a Heads-Up Display, we could just ask the game to render a set of characters somewhere but we want to have it not render it in weird ways. The best way would be to add an other layer to render.
Adding Another Rendering Layer
First we need to add in an other layer to the BTermBuilder::new() within the main.rs.
let context = BTermBuilder::new()
.with_title("Rusty Roguelike")
.with_fps_cap(FRAME_DURATION)
.with_dimensions(CONSOLE_WIDTH, CONSOLE_HEIGHT)
.with_tile_dimensions(TILE_SIZE, TILE_SIZE)
.with_resource_path("resources/")
.with_font("dungeonfont.png", 32, 32)
.with_font("terminal8x8.png", 8, 8)
.with_simple_console(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png")
.with_simple_console_no_bg(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png")
.with_simple_console_no_bg(DISPLAY_WIDTH * 2, DISPLAY_HEIGHT * 2, "terminal8x8.png")
.build()?;
Okay so let’s go over this: we need to make sure that we define the fonts that we are going to use (.with_font(“terminal8x8.png”, 8, 8)), then we need to define the new console in the other we want to have them labeled (.with_simple_console_no_bg(SCREEN_WIDTH 2, SCREEN_HEIGHT 2, “terminal8x8.png”)).
One of the issues now is that we will have an other layer to deal with. We will not need to be sure that we clear all layers before rendering an other tick(), let’s deal with that now. Still within main.rs let’s add that line to any render.
fn on_map(&mut self, ctx: &mut BTerm) {
ctx.set_active_console(0);
ctx.cls();
ctx.set_active_console(1);
ctx.cls();
ctx.set_active_console(2);
ctx.cls();
Rendering the HUD
Now we get to make an other system. Let’s create an other file then as always we need to add that to the mod.rs systems list and then all the system builders before the endturn. Create this file _src/systems/hud.rs.
// hud.rs
use crate::prelude::*;
#[system]
#[read_component(Health)]
#[read_component(Player)]
pub fn hud(ecs: &SubWorld) {
let mut health_query = <&Health>::query().filter(component::<Player>());
let player_health = health_query.iter(ecs).nth(0).unwrap();
let mut draw_batch = DrawBatch::new();
draw_batch.target(2);
draw_batch.print_centered(1, "Explore the Dungeon. Cursor keys to move.");
draw_batch.bar_horizontal(
Point::zero(),
SCREEN_WIDTH * 2,
player_health.current,
player_health.max,
ColorPair::new(RED, BLACK),
);
draw_batch.print_color_centered(
0,
format!(
" Health: {} / {} ",
player_health.current, player_health.max
),
ColorPair::new(WHITE, RED),
);
draw_batch.submit(10000).expect("Batch error");
}
// mod.rs
use crate::prelude::*;
mod collisions;
mod end_turn;
mod entity_render;
mod hud;
mod map_render;
mod movement;
mod player_input;
mod random_move;
pub fn build_input_scheduler() -> Schedule {
Schedule::builder()
.add_system(player_input::player_input_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(hud::hud_system())
.build()
}
pub fn build_player_scheduler() -> Schedule {
Schedule::builder()
.add_system(movement::movement_system())
.flush()
.add_system(collisions::collisions_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(hud::hud_system())
.add_system(end_turn::end_turn_system())
.build()
}
pub fn build_monster_scheduler() -> Schedule {
Schedule::builder()
.add_system(random_move::random_move_system())
.flush()
.add_system(movement::movement_system())
.flush()
.add_system(collisions::collisions_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(hud::hud_system())
.add_system(end_turn::end_turn_system())
.build()
}
So we added in the new HUD: very similar to many other systems, set the components that we need to read/write, .nth() is a built in Rust feature, we target the 2th layer, we then set up all the values that we want to print. For the mod.rs we need to add in the HUD to each set of renders for the given player_turn value.
Naming Monsters
We can now use that HUD to display other bits of information. In this case we want to display the monster name and hotpoint when needed. Let’s first add in the component for the Name. Head back to the component.rs
// components.rs
#[derive(Clone, PartialEq)]
pub struct Name(pub String);
Notice that we can normally have a tuple for the definition of the struct but if we have a single value we can just use the () syntax.
Giving Monsters Names and Hit Points
Going to the spawner.rs we see that the only thing that we have for each monster is a glyph. We now have a new struct for a name so let’s start to build a better builder for each monster type.
pub fn spawn_monster(ecs: &mut World, rng: &mut RandomNumberGenerator, pos: Point) {
let (hp, name, glyph) = match rng.roll_dice(1, 10) {
1..=8 => goblin(),
_ => orc(),
};
ecs.push((
Enemy,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph,
},
MovingRandomly {},
Health {
current: hp,
max: hp,
},
Name(name),
));
}
fn goblin() -> (i32, String, FontCharType) {
(1, "Goblin".to_string(), to_cp437('g'))
}
fn orc() -> (i32, String, FontCharType) {
(2, "Orc".to_string(), to_cp437('o'))
}
Okay so we added in some helper functions that will return a tuple describing the monsters. We then changed the spawn_monster so that we can use the new functions: we ask for a dice rng, and depending on which value we get back we add a goblin() or an orc() with that we then add the monster with the values we pass into the ecs.push()
Identifying Monsters with Tooltips
We can now add in the ability to see where the cursor is. We added in the keyboard state well there is a way to see where the mouse is too. Add these lines to the place where you added in the keyboard.
self.resources.insert(ctx.key);
ctx.set_active_console(0);
self.resources.insert(Point::from_tuple(ctx.mouse_pos()));
Okay now we are watching for the mouse on the first (0) render console. Let’s add in an other system that will watch for the mouse position and then use that to display the monster information that is at the current mouse position. Create this file src/systems/tooltips.rs.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(Name)]
#[read_component(Health)]
pub fn tooltips(ecs: &SubWorld, #[resource] mouse_pos: &Point, #[resource] camera: &Camera) {
let mut positions = <(Entity, &Point, &Name)>::query();
let offset = Point::new(camera.left_x, camera.top_y);
let map_pos = *mouse_pos + offset;
let mut draw_batch = DrawBatch::new();
draw_batch.target(2);
positions
.iter(ecs)
.filter(|(_, pos, _)| **pos == map_pos)
.for_each(|(entity, _, name)| {
let screen_pos = *mouse_pos * 2;
let display =
if let Ok(health) = ecs.entry_ref(*entity).unwrap().get_component::<Health>() {
format!("{} : {} hp", &name.0, health.current)
} else {
name.0.clone()
};
draw_batch.print(screen_pos, &display);
});
draw_batch.submit(10100).expect("Batch error");
}
Register Your Systems
You might have already registered the HUD, but make sure that we have the HUD and the Tooltip added to the mod.rs.
use crate::prelude::*;
mod collisions;
mod end_turn;
mod entity_render;
mod hud;
mod map_render;
mod movement;
mod player_input;
mod random_move;
mod tooltips;
pub fn build_input_scheduler() -> Schedule {
Schedule::builder()
.add_system(player_input::player_input_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(hud::hud_system())
.add_system(tooltips::tooltips_system())
.build()
}
pub fn build_player_scheduler() -> Schedule {
Schedule::builder()
.add_system(movement::movement_system())
.flush()
.add_system(collisions::collisions_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(hud::hud_system())
.add_system(tooltips::tooltips_system())
.add_system(end_turn::end_turn_system())
.build()
}
pub fn build_monster_scheduler() -> Schedule {
Schedule::builder()
.add_system(random_move::random_move_system())
.flush()
.add_system(movement::movement_system())
.flush()
.add_system(collisions::collisions_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(hud::hud_system())
.add_system(tooltips::tooltips_system())
.add_system(end_turn::end_turn_system())
.build()
}
So a few things happened here. We added the ability to show a monsters information (tooltip) on the screen. This utilizes the offset and the mouse position to know where the monster and mouse position are. Once we have that we need to display the information on the screen in the right spot. Then as always we need to add the system to the build.
Implementing Combat
We now need to implement a combat system that utilizes messages. This will allow use to take away the collision.rs and allow a more robust combat system.
Remove the Collision System
First let’s remove the collision.rs this is just there to remove a mob and is no longer needed. There will be some things that break: mod.rs has some systems that are trying to be used feel free to comment out those and the mod collisions as well.
Indicating Intent to Attack
Head to components.rs and add the following this is for the WantsToAttack message.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WantsToAttack {
pub attacker: Entity,
pub victim: Entity,
}
Player Move to Attack
For this game when we are on the same space as a monster or if we try to move into a monster we are going to attack the monster. Let’s head to player_input.rs and change it so its not just a check to see if we can move to a location but will initiate an attack if we try to move and a monster is there.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(Player)]
#[read_component(Enemy)]
pub fn player_input(
ecs: &SubWorld,
commands: &mut CommandBuffer,
#[resource] key: &Option<VirtualKeyCode>,
#[resource] turn_state: &mut TurnState,
) {
let mut players = <(Entity, &Point)>::query().filter(component::<Player>());
if let Some(key) = *key {
let delta = match key {
VirtualKeyCode::Left => Point::new(-1, 0),
VirtualKeyCode::Right => Point::new(1, 0),
VirtualKeyCode::Up => Point::new(0, -1),
VirtualKeyCode::Down => Point::new(0, 1),
_ => Point::new(0, 0),
};
let (player_entity, destination) = players
.iter(ecs)
.find_map(|(entity, pos)| Some((*entity, *pos + delta)))
.unwrap();
let mut enemies = <(Entity, &Point)>::query().filter(component::<Enemy>());
if delta.x != 0 || delta.y != 0 {
let mut hit_something = false;
enemies
.iter(ecs)
.filter(|(_, pos)| **pos == destination)
.for_each(|(entity, _)| {
hit_something = true;
commands.push((
(),
WantsToAttack {
attacker: player_entity,
victim: *entity,
},
));
});
if !hit_something {
commands.push((
(),
WantsToMove {
entity: player_entity,
destination,
},
));
}
}
*turn_state = TurnState::PlayerTurn;
}
}
So looking at this code we see a lot of things. First we don’t blindly attempt to move the player. Let’s go over how that works here. First we unwrap the player and the destination from the players query, then we find all the enemies on the map, then we run an other query that will filter out only those enemies that share the same location as the destination, if we get a hit we turn the hit_something flag to true then issue a WantsToAttack message, otherwise we simple issue the WantsToMove message.
Creating a Combat System
This is where the rubber will hit the road. For right now we will just have an attack of 1, but we can even build different attacks as components later. Let’s start by creating a new system src/systems/combat.rs
use crate::prelude::*;
#[system]
#[read_component(WantsToAttack)]
#[write_component(Health)]
pub fn combat(ecs: &mut SubWorld, commands: &mut CommandBuffer) {
let mut attackers = <(Entity, &WantsToAttack)>::query();
let victims: Vec<(Entity, Entity)> = attackers
.iter(ecs)
.map(|(entity, attack)| (*entity, attack.victim))
.collect();
victims.iter().for_each(|(message, victim)| {
if let Ok(mut health) = ecs
.entry_mut(*victim)
.unwrap()
.get_component_mut::<Health>()
{
println!("Health before attack: {}", health.current);
health.current -= 1;
if health.current < 1 {
commands.remove(*victim);
}
println!("Health after attack: {}", health.current);
}
commands.remove(*message);
});
}
We added in the read and write access and then did the following: queried the WantsToAttack entities, we then have to get the victims by saying the the attacks will follow the vector of <(Entity, Entity)>, we then need to iterate over the list of victims removing health.
The Monsters Strike Back
Okay so we now have a system for attacking monsters but they should be able to fight back (we also haven’t added the system to mod.rs yet). Let’s change the random_move.rs to allow for the monsters to attack a player. Right now we just have random move but this will change to have the monsters chase later.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(MovingRandomly)]
#[read_component(Health)]
#[read_component(Player)]
pub fn random_move(ecs: &SubWorld, commands: &mut CommandBuffer) {
let mut movers = <(Entity, &Point, &MovingRandomly)>::query();
let mut positions = <(Entity, &Point, &Health)>::query();
movers.iter(ecs).for_each(|(entity, pos, _)| {
let mut rng = RandomNumberGenerator::new();
let destination = match rng.range(0, 4) {
0 => Point::new(-1, 0),
1 => Point::new(1, 0),
2 => Point::new(0, -1),
_ => Point::new(0, 1),
} + *pos;
let mut attacked = false;
positions
.iter(ecs)
.filter(|(_, target_pos, _)| **target_pos == destination)
.for_each(|(victim, _, _)| {
if ecs
.entry_ref(*victim)
.unwrap()
.get_component::<Player>()
.is_ok()
{
commands.push((
(),
WantsToAttack {
attacker: *entity,
victim: *victim,
},
));
}
attacked = true;
});
if !attacked {
commands.push((
(),
WantsToMove {
entity: *entity,
destination,
},
));
}
});
}
So for this we now will set the new position for the move, once we have that we check to see if there is a entity there, We then for each entity in the target position issue a WantsToAttack and set the attacked to TRUE, if attacked is false we will simply issue a WantsToMove message.
Running the System
Let’s add the new systems to the mod.rs and then try out our game so far.
// mod.rs
use crate::prelude::*;
mod combat;
mod end_turn;
mod entity_render;
mod hud;
mod map_render;
mod movement;
mod player_input;
mod random_move;
mod tooltips;
pub fn build_input_scheduler() -> Schedule {
Schedule::builder()
.add_system(player_input::player_input_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(hud::hud_system())
.add_system(tooltips::tooltips_system())
.build()
}
pub fn build_player_scheduler() -> Schedule {
Schedule::builder()
.add_system(combat::combat_system())
.flush()
.add_system(movement::movement_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(hud::hud_system())
.add_system(end_turn::end_turn_system())
.build()
}
pub fn build_monster_scheduler() -> Schedule {
Schedule::builder()
.add_system(random_move::random_move_system())
.flush()
.add_system(combat::combat_system())
.flush()
.add_system(movement::movement_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(hud::hud_system())
.add_system(end_turn::end_turn_system())
.build()
}
We didn’t need to change the input just the player and monster systems.
Waiting as a Strategy
Lastly we will build a waiting system so a players health can go up for not doing anything. We will do this by setting a did_something variable to true if we did something. This will trigger on a movement or an attack. Head to player_input.rs and let’s tweak it a bit.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(Player)]
#[read_component(Enemy)]
#[write_component(Health)]
pub fn player_input(
ecs: &SubWorld,
commands: &mut CommandBuffer,
#[resource] key: &Option<VirtualKeyCode>,
#[resource] turn_state: &mut TurnState,
) {
let mut players = <(Entity, &Point)>::query().filter(component::<Player>());
if let Some(key) = *key {
let delta = match key {
VirtualKeyCode::Left => Point::new(-1, 0),
VirtualKeyCode::Right => Point::new(1, 0),
VirtualKeyCode::Up => Point::new(0, -1),
VirtualKeyCode::Down => Point::new(0, 1),
_ => Point::new(0, 0),
};
let (player_entity, destination) = players
.iter(ecs)
.find_map(|(entity, pos)| Some((*entity, *pos + delta)))
.unwrap();
let mut enemies = <(Entity, &Point)>::query().filter(component::<Enemy>());
let mut did_something = false;
if delta.x != 0 || delta.y != 0 {
let mut hit_something = false;
enemies
.iter(ecs)
.filter(|(_, pos)| **pos == destination)
.for_each(|(entity, _)| {
hit_something = true;
did_something = true;
commands.push((
(),
WantsToAttack {
attacker: player_entity,
victim: *entity,
},
));
});
if !hit_something {
did_something = true;
commands.push((
(),
WantsToMove {
entity: player_entity,
destination,
},
));
}
}
if !did_something {
if let Ok(mut health) = ecs
.entry_mut(player_entity)
.unwrap()
.get_component_mut::<Health>()
{
health.current = i32::min(health.max, health.current + 1);
}
}
*turn_state = TurnState::PlayerTurn;
}
}
We added in the ability now to do nothing and get some health back. We set the did_something to false to start and follow the rules from above. The last thing we did is query the health of the player and added 1 to the current.health and set the value of the current_health to the min of max and the new value. For this to work you need to activate a non movement action, (SPACEBAR).
Wrap-Up
Test our your game!!! Look at all the work we did for this. We now have 2 types of message systems. We can now start to think about ways that we will work with new things in the future.