We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
9. Victory and Defeat
Let’s start off with building a better monster. What does this mean? Well a monster should not just randomly walk around once it notices a Player, there should be s threshold in which it should then start to go after the player.
We will then build an End of Game condition. This will be something in which we will at least at this point build what happens when we lose a game. Once you are out of health there should be something that happens. Otherwise you will not have a challenge.
Lastly we will build the Winning Condition in which the player will be able to find the Amulet of Yala. This is an item in the dungeon that will spawn far away from the player and will need to be picked up by the player over the course of searching the dungeon. Let’s get started.
Building a Smarter Monster
Let’s start here. This will be be us building a “heat-seeking” behavior.
Tagging the New Behavior
As with everything else you need to build a component for the new feature and eventually the system that we will build. I will take a second here to go over the idea of anything that we build. We will query an entity (or more than one entity) and then see about other resources that we might need to read/write. This will be then added to the build for a given tick. Once you have the system it will be part of the tick, like in the case of the “heat-seeking“, we will build a way to determine when a player is in range and then it will continue to follow the player from that point forward. To make this a thing that will continue to happen we will trigger a ChasingPlayer tag.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ChasingPlayer;
A simple struct that has a name. Now we can just add that to a spawned monster so they will chase the player from the beginning (once we have a way of determining distance we can make them only chase the player once they are in “range”).
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,
},
ChasingPlayer {},
Health {
current: hp,
max: hp,
},
Name(name),
));
}
Supporting Pathfinding with Traits
First let’s talk about a trait:
A trait tells the Rust compiler about functionality a particular type has and can
share with other types. You can use traits to define shared behavior in an abstract
way. You can use trait bounds to specify that a generic can be any type that has
certain behavior.
What does this mean? Well it says that once we build a trait that a compiler or a library (crate) is expecting and in a fashion that it can use you don’t have to worry about the exact implementation that it will be used by the crate. We have then abstracted it out.
Mapping the Map
For us we want to use the Algorithm2D to take our map and translate it to something that bracket-lib will be able to use. Some of the defaults that we want are:
• dimensions provides the size of the map.
• in_bounds determines if an x/y coordinate is valid and contained within the
map.
• point2d_to_index is the same as your map_idx function; it converts a 2D x/y
coordinate to a map index.
• index_to_point2d is the reciprocal of point2d_to_index; given a map index, it
returns the x/y coordinates of that point.
Open up map.rs and add the following to start to define this needed traits.
impl Algorithm2D for Map {
fn dimensions(&self) -> Point {
Point::new(CONSOLE_WIDTH, CONSOLE_HEIGHT)
}
fn in_bounds(&self, point: Point) -> bool {
self.in_bounds(point)
}
}
This is simply setting up the overall size of the map.
Navigating the Map
Let’s now start to work with the second trait BaseMap, you will need to define 2 functions for it: available_exits() and get_pathing_distance(). These will be used to define entrances and exits from a tile and a way to determine distance from one point to an other. Let’s add these to the impl Map block in map.rs
fn valid_exit(&self, loc: Point, delta: Point) -> Option<usize> {
let destination = loc + delta;
if self.in_bounds(destination) {
if self.can_enter_tile(destination) {
let idx = self.point2d_to_index(destination);
Some(idx)
} else {
None
}
} else {
None
}
}
This allows us to return an option for the function. None if it fails and Some if it passes. You’ve seen this before in Advanced Functional Elixir.
Next we can define the implementation for get_available_exits()
impl BaseMap for Map {
fn get_available_exits(&self, idx: usize) -> SmallVec<[(usize, f32); 10]> {
let mut exits = SmallVec::new();
let location = self.index_to_point2d(idx);
if let Some(idx) = self.valid_exit(location, Point::new(-1, 0)) {
exits.push((idx, 1.0))
}
if let Some(idx) = self.valid_exit(location, Point::new(1, 0)) {
exits.push((idx, 1.0))
}
if let Some(idx) = self.valid_exit(location, Point::new(0, -1)) {
exits.push((idx, 1.0))
}
if let Some(idx) = self.valid_exit(location, Point::new(0, 1)) {
exits.push((idx, 1.0))
}
exits
}
}
SmallVec is a more resource efferent way of storing a vector, we then take the idx (index of the point) and turn it into a 2d point, we then push all the points around the location if valid as an exit.
Now we can add the get_pathing_distance implementation to the BaseMap
fn get_pathing_distance(&self, idx1: usize, idx2: usize) -> f32 {
DistanceAlg::Pythagoras.distance2d(self.index_to_point2d(idx1), self.index_to_point2d(idx2))
}
Heat-Seeking Monsters
Let’s add in a new file that will be responsible for have a monster go after a player. Create src/systems/chasing.rs this is the system for that ChasingPlayer component that we made.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(ChasingPlayer)]
#[read_component(Health)]
#[read_component(Player)]
pub fn chasing(#[resource] map: &Map, ecs: &SubWorld, commands: &mut CommandBuffer) {
// Setting up the queries
let mut movers = <(Entity, &Point, &ChasingPlayer)>::query();
let mut positions = <(Entity, &Point, &Health)>::query();
let mut player = <(&Point, &Player)>::query();
// Finding the Player
let player_pos = player.iter(ecs).nth(0).unwrap().0;
let player_idx = map_idx(player_pos.x, player_pos.y);
//Dijkstra Maps
let search_targets = vec![player_idx];
let dijkstra_map = DijkstraMap::new(CONSOLE_WIDTH, CONSOLE_HEIGHT, &search_targets, map, 1024.0);
// Chasing the Player
movers.iter(ecs).for_each(|(entity, pos, _)| {
let idx = map_idx(pos.x, pos.y);
if let Some(destination) = DijkstraMap::find_lowest_exit(&dijkstra_map, idx, map) {
let distance = DistanceAlg::Pythagoras.distance2d(*pos, *player_pos);
let destination = if distance > 1.2 {
map.index_to_point2d(destination)
} else {
*player_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,
},
));
}
}
});
}
We will go over this one set of comments at a time. Lets start with the queries that you have seen many times before.
Finding the Player
The next block of code is for finding the player. We take the player and then convert there position into an idx for the system to work.
Dijkstra Maps
Basically we are going to build the map into a series of numbers that represent the distance from the player. We can then use this for many different things going forward. It will also help a monster determine which direction to travel in order to “Chase” the player as a decrease in the Dijkstra number means they are getting closer to the player.
Going to the next code block we see: We take a set of targets in this case the player, then we build the map with some key inputs; CONSOLE_WIDTH/HEIGHT, the Map, and a distance in which we will build the numbers.
Chasing the Player
Looking at the next coding block this is where we want to start to use the map: first we are trying to get all the movers (monsters), then we take the Dijkstra map and use a defined function find_lowest_exit, we take that new point and calculate the distance from the player to the point, if it is greater than the 1.2 ie a diagonal or a many tiles move to the find_lowest_exit, otherwise we move to the player position.
The rest is something very similar we do checks for attacking the player or simply moving.
What is left now? That is right adding the new system to the game and then Testing it out!!!
mod chasing;
...
pub fn build_monster_scheduler() -> Schedule {
Schedule::builder()
.add_system(random_move::random_move_system())
.add_system(chasing::chasing_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()
}
Reducing the Players’s Health
Now let’s make the game a little bit more tough. Let’s go to the spawner and make the player health 10.
pub fn spawn_player(ecs: &mut World, pos: Point) {
ecs.push((
Player,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph: to_cp437('@'),
},
Health {
current: 10,
max: 10,
},
));
}
Implementing a Game Over Screen
Okay so we now have a much harder game, but every time we get to 0 health the game just ends. We need to do something about that. First let’s just add in a clause for the player getting to 0 health.
// combat.rs
use crate::prelude::*;
#[system]
#[read_component(WantsToAttack)]
#[read_component(Player)]
#[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)| {
let is_player = ecs
.entry_ref(*victim)
.unwrap()
.get_component::<Player>()
.is_ok();
if let Ok(mut health) = ecs
.entry_mut(*victim)
.unwrap()
.get_component_mut::<Health>()
{
health.current -= 1;
if health.current < 1 && !is_player {
commands.remove(*victim);
}
}
commands.remove(*message);
});
}
Now we simply will not remove the player in the case of the player dyeing as the HUD will crash if we remove the player.
Adding a Game Over Turn State
Let’s add a different turn State. I would think it would be better here to update the GameState to reflect a GAMEOVER, but for now we can try and make it a TurnState. Let’s add the new state to the Enum.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum TurnState {
AwaitingInput,
PlayerTurn,
MonsterTurn,
GameOver,
}
Did you get some errors? Well that is because in Rust all Enums must have a case so that there is no warnings or errors. We need to take care of those now.
warning: variable does not need to be mutable
--> src/systems/combat.rs:18:19
|
18 | if let Ok(mut health) = ecs
| ----^^^^^^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default
error[E0004]: non-exhaustive patterns: `&mut turn_state::TurnState::GameOver` not covered
--> src/systems/end_turn.rs:4:27
|
4 | let new_state = match turn_state {
| ^^^^^^^^^^ pattern `&mut turn_state::TurnState::GameOver` not covered
|
note: `turn_state::TurnState` defined here
--> src/turn_state.rs:6:5
|
2 | pub enum TurnState {
| ---------
...
6 | GameOver,
| ^^^^^^^^ not covered
= note: the matched value is of type `&mut turn_state::TurnState`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
7 ~ TurnState::MonsterTurn => TurnState::AwaitingInput,
8 ~ &mut turn_state::TurnState::GameOver => todo!(),
|
warning: variable does not need to be mutable
--> src/systems/player_input.rs:61:23
|
61 | if let Ok(mut health) = ecs
| ----^^^^^^
| |
| help: remove this `mut`
error[E0004]: non-exhaustive patterns: `turn_state::TurnState::GameOver` not covered
--> src/main.rs:93:15
|
93 | match current_state {
| ^^^^^^^^^^^^^ pattern `turn_state::TurnState::GameOver` not covered
|
note: `turn_state::TurnState` defined here
--> src/turn_state.rs:6:5
|
2 | pub enum TurnState {
| ---------
...
6 | GameOver,
| ^^^^^^^^ not covered
= note: the matched value is of type `turn_state::TurnState`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
103~ .execute(&mut self.ecs, &mut self.resources),
104~ turn_state::TurnState::GameOver => todo!(),
|
Those are the errors that I got lets take care of all the errors in one swoop.
// end_turn.rs
use crate::prelude::*;
#[system]
#[read_component(Health)]
#[read_component(Player)]
pub fn end_turn(ecs: &SubWorld, #[resource] turn_state: &mut TurnState) {
let mut player_hp = <&Health>::query().filter(component::<Player>());
let current_state = turn_state.clone();
let mut new_state = match current_state {
TurnState::AwaitingInput => return,
TurnState::PlayerTurn => TurnState::MonsterTurn,
TurnState::MonsterTurn => TurnState::AwaitingInput,
_ => current_state,
};
player_hp.iter(ecs).for_each(|hp| {
if hp.current < 1 {
new_state = TurnState::GameOver;
}
});
*turn_state = new_state;
}
Before we just had a way of taking the current state and transferring it to the next state now we have some of the same implementation but now we have a check at the end for a GameOver.
Displaying a Game Over Screen
impl State {
...
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();
self.resources.insert(ctx.key);
ctx.set_active_console(0);
self.resources.insert(Point::from_tuple(ctx.mouse_pos()));
let current_state = self.resources.get::<TurnState>().unwrap().clone();
match current_state {
TurnState::AwaitingInput => self
.input_systems
.execute(&mut self.ecs, &mut self.resources),
TurnState::PlayerTurn => {
self.player_systems
.execute(&mut self.ecs, &mut self.resources);
}
TurnState::MonsterTurn => self
.monster_systems
.execute(&mut self.ecs, &mut self.resources),
TurnState::GameOver => {
self.game_over(ctx);
}
}
render_draw_buffer(ctx).expect("Render error");
}
fn game_over(&mut self, ctx: &mut BTerm) {
ctx.set_active_console(2);
ctx.print_color_centered(2, RED, BLACK, "Your quest has ended.");
ctx.print_color_centered(
4,
WHITE,
BLACK,
"Slain by a monster, your hero's journey has come to a \
premature end.",
);
ctx.print_color_centered(
5,
WHITE,
BLACK,
"The Amulet of Yala remains unclaimed, and your home town \
is not saved.",
);
ctx.print_color_centered(
8,
YELLOW,
BLACK,
"Don't worry, you can always try again with a new hero.",
);
ctx.print_color_centered(9, GREEN, BLACK, "Press 1 to play again.");
if let Some(VirtualKeyCode::Key1) = ctx.key {
self.ecs = World::default();
self.resources = Resources::default();
let mut rng = RandomNumberGenerator::new();
let map_builder = MapBuilder::new(&mut rng);
spawn_player(&mut self.ecs, map_builder.player_start);
map_builder
.rooms
.iter()
.skip(1)
.map(|r| r.center())
.for_each(|pos| spawn_monster(&mut self.ecs, &mut rng, pos));
self.resources.insert(map_builder.map);
self.resources.insert(Camera::new(map_builder.player_start));
self.resources.insert(TurnState::AwaitingInput);
}
}
}
Okay we now can die and restart. But losing isn’t as fun as winning so let’s work on that.
Finding the Amulet of Yala
In order to win the game you need to find the Amulet of Yala. You know the process, component, system, mod, etc.
Spawning the Amulet
You will need to different components for this as it not just and item its THE item.
// component.rs
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Item;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AmuletOfYala;
// spawner.rs
pub fn spawn_amulet_of_yala(ecs: &mut World, pos: Point) {
ecs.push((
Item,
AmuletOfYala,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph: to_cp437('|'),
},
Name("Amulet of Yala".to_string()),
));
}
We now have a way to spawn the amulet but we don’t want it just anywhere. We want it to be placed in a specific spot away from the player.
Placing the Amulet
Let’s find a way to place it not the lowest but the highest distance from the player. Let’s open up the map_builder.rs and start to work on adding in the Amulet.
pub struct MapBuilder {
pub map: Map,
pub rooms: Vec<Rect>,
pub player_start: Point,
pub amulet_start: Point,
}
impl MapBuilder {
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
let mut mb = MapBuilder {
map: Map::new(),
rooms: Vec::new(),
player_start: Point::zero(),
amulet_start: Point::zero(),
};
mb.fill(TileType::Wall);
mb.build_random_rooms(rng);
mb.build_corridors(rng);
mb.player_start = mb.rooms[0].center();
let dijkstra_map = DijkstraMap::new(
CONSOLE_WIDTH,
CONSOLE_HEIGHT,
&vec![mb.map.point2d_to_index(mb.player_start)],
&mb.map,
1024.0,
);
const UNREACHABLE: &f32 = &f32::MAX;
mb.amulet_start = mb.map.index_to_point2d(
dijkstra_map
.map
.iter()
.enumerate()
.filter(|(_, dist)| *dist < UNREACHABLE)
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap()
.0,
);
mb
}
...
}
// main.rs in 2 places you spawn a player spawn the amulet too
spawn_player(&mut self.ecs, map_builder.player_start);
spawn_amulet_of_yala(&mut self.ecs, map_builder.amulet_start);
This feels like a lot but what did we really do? We found the furthest distance from the player and then placed the amulet there using Dijkstra.
Determining if the Player Won
Okay so we need to add in an other TurnState and then give the clauses for it and then work on the victory screen. I’m going to put all the code here for the determination of the victory then after we can work on the victory screen.
// turn_state.rs
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum TurnState {
AwaitingInput,
PlayerTurn,
MonsterTurn,
GameOver,
Victory,
}
// end_turn.rs
use crate::prelude::*;
#[system]
#[read_component(Health)]
#[read_component(Point)]
#[read_component(Player)]
#[read_component(AmuletOfYala)]
pub fn end_turn(ecs: &SubWorld, #[resource] turn_state: &mut TurnState) {
let mut player_hp = <(&Health, &Point)>::query().filter(component::<Player>());
let mut amulet = <&Point>::query().filter(component::<AmuletOfYala>());
let amulet_pos = amulet.iter(ecs).nth(0).unwrap();
let current_state = turn_state.clone();
let mut new_state = match current_state {
TurnState::AwaitingInput => return,
TurnState::PlayerTurn => TurnState::MonsterTurn,
TurnState::MonsterTurn => TurnState::AwaitingInput,
_ => current_state,
};
player_hp.iter(ecs).for_each(|(hp, pos)| {
if hp.current < 1 {
new_state = TurnState::GameOver;
}
if pos == amulet_pos {
new_state = TurnState::Victory;
}
});
*turn_state = new_state;
}
Lots we have seen before setting up the read and then queries for the components, then checking to see if the player is in the right condition.
Congratulating the Player
Okay a couple things that we want to do here, first we should try to think DRY (Don’t Repeat Yourself) so lets put the reset_game_state into a single function, then we can build the victory and clearn up the game_over clause.
impl State {
...
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();
self.resources.insert(ctx.key);
ctx.set_active_console(0);
self.resources.insert(Point::from_tuple(ctx.mouse_pos()));
let current_state = self.resources.get::<TurnState>().unwrap().clone();
match current_state {
TurnState::AwaitingInput => self
.input_systems
.execute(&mut self.ecs, &mut self.resources),
TurnState::PlayerTurn => {
self.player_systems
.execute(&mut self.ecs, &mut self.resources);
}
TurnState::MonsterTurn => self
.monster_systems
.execute(&mut self.ecs, &mut self.resources),
TurnState::GameOver => self.game_over(ctx),
TurnState::Victory => self.victory(ctx),
}
render_draw_buffer(ctx).expect("Render error");
}
fn game_over(&mut self, ctx: &mut BTerm) {
ctx.set_active_console(2);
ctx.print_color_centered(2, RED, BLACK, "Your quest has ended.");
ctx.print_color_centered(
4,
WHITE,
BLACK,
"Slain by a monster, your hero's journey has come to a \
premature end.",
);
ctx.print_color_centered(
5,
WHITE,
BLACK,
"The Amulet of Yala remains unclaimed, and your home town \
is not saved.",
);
ctx.print_color_centered(
8,
YELLOW,
BLACK,
"Don't worry, you can always try again with a new hero.",
);
ctx.print_color_centered(
9,
GREEN,
BLACK,
"Press 1 to play \
again.",
);
if let Some(VirtualKeyCode::Key1) = ctx.key {
self.reset_game_state();
}
}
fn victory(&mut self, ctx: &mut BTerm) {
ctx.set_active_console(2);
ctx.print_color_centered(2, GREEN, BLACK, "You have won!");
ctx.print_color_centered(
4,
WHITE,
BLACK,
"You put on the Amulet of Yala and feel its power course through \
your veins.",
);
ctx.print_color_centered(
5,
WHITE,
BLACK,
"Your town is saved, and you can return to your normal life.",
);
ctx.print_color_centered(
7,
GREEN,
BLACK,
"Press 1 to \
play again.",
);
if let Some(VirtualKeyCode::Key1) = ctx.key {
self.reset_game_state();
}
}
fn reset_game_state(&mut self) {
self.ecs = World::default();
self.resources = Resources::default();
let mut rng = RandomNumberGenerator::new();
let map_builder = MapBuilder::new(&mut rng);
spawn_player(&mut self.ecs, map_builder.player_start);
spawn_amulet_of_yala(&mut self.ecs, map_builder.amulet_start);
map_builder
.rooms
.iter()
.skip(1)
.map(|r| r.center())
.for_each(|pos| spawn_monster(&mut self.ecs, &mut rng, pos));
self.resources.insert(map_builder.map);
self.resources.insert(Camera::new(map_builder.player_start));
self.resources.insert(TurnState::AwaitingInput);
}
}
Everything that you have seen before.
Wrap-Up
Okay test out your game and then check for errors. Does it make sense to add more health? Less Health? You have come a long way don’t stop now.