From 598032aadfb7b053fc74480bc2c8d60e65a3ed7a Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Fri, 29 Oct 2021 15:15:22 -0400 Subject: [PATCH] Finish chapter 2.6 --- src/components.rs | 31 +++++++++ src/damage_system.rs | 47 ++++++++++++++ src/main.rs | 126 ++++++++++++++++++++++++++----------- src/map.rs | 8 +++ src/map_indexing_system.rs | 17 ++++- src/melee_combat_system.rs | 46 ++++++++++++++ src/monster_ai_system.rs | 48 ++++++++++---- src/player.rs | 44 ++++++++++--- 8 files changed, 306 insertions(+), 61 deletions(-) create mode 100644 src/damage_system.rs create mode 100644 src/melee_combat_system.rs diff --git a/src/components.rs b/src/components.rs index 0e40c75..c2c6f12 100644 --- a/src/components.rs +++ b/src/components.rs @@ -35,3 +35,34 @@ pub struct Name { #[derive(Component)] pub struct BlocksTile {} + +#[derive(Component)] +pub struct CombatStats { + pub max_hp: i32, + pub hp: i32, + pub defense: i32, + pub power: i32, +} + +#[derive(Component, Debug, Clone)] +pub struct WantsToMelee { + pub target: Entity, +} + +#[derive(Component, Debug)] +pub struct SufferDamage { + pub amount: Vec, +} + +impl SufferDamage { + pub fn new_damage(store: &mut WriteStorage, victim: Entity, amount: i32) { + if let Some(suffering) = store.get_mut(victim) { + suffering.amount.push(amount); + } else { + let dmg = SufferDamage { + amount: vec![amount], + }; + store.insert(victim, dmg).expect("Unable to insert damage"); + } + } +} diff --git a/src/damage_system.rs b/src/damage_system.rs new file mode 100644 index 0000000..26a71b6 --- /dev/null +++ b/src/damage_system.rs @@ -0,0 +1,47 @@ +use crate::{CombatStats, Player, SufferDamage}; +use rltk::console; +use specs::prelude::*; + +pub struct DamageSystem {} + +impl<'a> System<'a> for DamageSystem { + type SystemData = ( + WriteStorage<'a, CombatStats>, + WriteStorage<'a, SufferDamage>, + ); + + fn run(&mut self, data: Self::SystemData) { + let (mut stats, mut damage) = data; + + for (mut stats, damage) in (&mut stats, &damage).join() { + stats.hp -= damage.amount.iter().sum::(); + } + + damage.clear(); + } +} + +pub fn delete_the_dead(ecs: &mut World) { + let mut dead: Vec = Vec::new(); + + // Scope for the sake of the borrow checker + { + let combat_stats = ecs.read_storage::(); + let players = ecs.read_storage::(); + let entities = ecs.entities(); + for (entity, stats) in (&entities, &combat_stats).join() { + if stats.hp < 1 { + let player = players.get(entity); + match player { + None => dead.push(entity), + Some(_) => console::log("You are dead"), + } + } + } + } + + for victim in dead { + ecs.delete_entity(victim) + .expect("Unable to delete the dead"); + } +} diff --git a/src/main.rs b/src/main.rs index 1c16e46..9733737 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,21 +12,26 @@ pub use rect::Rect; mod visibility_system; use visibility_system::VisibilitySystem; mod monster_ai_system; -use monster_ai_system::*; +use monster_ai_system::MonsterAI; mod map_indexing_system; -use map_indexing_system::*; +use map_indexing_system::MapIndexingSystem; +mod melee_combat_system; +use melee_combat_system::MeleeCombatSystem; +mod damage_system; +use damage_system::DamageSystem; pub const MAP_SIZE: usize = 80 * 50; #[derive(PartialEq, Copy, Clone)] pub enum RunState { - Paused, - Running, + AwaitingInput, + PreRun, + PlayerTurn, + MonsterTurn, } pub struct State { pub ecs: World, - pub runstate: RunState, } impl State { @@ -40,6 +45,12 @@ impl State { let mut mapindex = MapIndexingSystem {}; mapindex.run_now(&self.ecs); + let mut melee = MeleeCombatSystem {}; + melee.run_now(&self.ecs); + + let mut damage = DamageSystem {}; + damage.run_now(&self.ecs); + self.ecs.maintain(); } } @@ -47,14 +58,37 @@ impl State { impl GameState for State { fn tick(&mut self, ctx: &mut Rltk) { ctx.cls(); - - if self.runstate == RunState::Running { - self.run_systems(); - self.runstate = RunState::Paused; - } else { - self.runstate = player_input(self, ctx); + let mut newrunstate; + { + let runstate = self.ecs.fetch::(); + newrunstate = *runstate; } + match newrunstate { + RunState::PreRun => { + self.run_systems(); + newrunstate = RunState::AwaitingInput; + } + RunState::AwaitingInput => { + newrunstate = player_input(self, ctx); + } + RunState::PlayerTurn => { + self.run_systems(); + newrunstate = RunState::MonsterTurn; + } + RunState::MonsterTurn => { + self.run_systems(); + newrunstate = RunState::AwaitingInput; + } + } + + { + let mut runwriter = self.ecs.write_resource::(); + *runwriter = newrunstate; + } + + damage_system::delete_the_dead(&mut self.ecs); + draw_map(&self.ecs, ctx); let positions = self.ecs.read_storage::(); @@ -63,8 +97,9 @@ impl GameState for State { for (pos, render) in (&positions, &renderables).join() { let idx = map.xy_idx(pos.x, pos.y); + if map.visible_tiles[idx] { - ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph); + ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) } } } @@ -77,10 +112,7 @@ fn main() -> rltk::BError { .with_title("Roguelike Tutorial") .build()?; - let mut gs = State { - ecs: World::new(), - runstate: RunState::Running, - }; + let mut gs = State { ecs: World::new() }; gs.ecs.register::(); gs.ecs.register::(); @@ -89,10 +121,42 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); let map: Map = Map::new_map_rooms_and_corridors(); let (player_x, player_y) = map.rooms[0].center(); + let player_entity = gs + .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), + }) + .with(Player {}) + .with(Viewshed { + visible_tiles: Vec::new(), + range: 8, + dirty: true, + }) + .with(Name { + name: "Player".to_string(), + }) + .with(CombatStats { + max_hp: 30, + hp: 30, + defense: 2, + power: 5, + }) + .build(); + let mut rng = rltk::RandomNumberGenerator::new(); for (i, room) in map.rooms.iter().skip(1).enumerate() { let (x, y) = room.center(); @@ -129,33 +193,19 @@ fn main() -> rltk::BError { name: format!("{} #{}", &name, i), }) .with(BlocksTile {}) + .with(CombatStats { + max_hp: 16, + hp: 16, + defense: 1, + power: 4, + }) .build(); } - gs.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), - }) - .with(Player {}) - .with(Viewshed { - visible_tiles: Vec::new(), - range: 8, - dirty: true, - }) - .with(Name { - name: "Player".to_string(), - }) - .build(); - gs.ecs.insert(map); gs.ecs.insert(Point::new(player_x, player_y)); + gs.ecs.insert(player_entity); + gs.ecs.insert(RunState::PreRun); rltk::main_loop(context, gs) } diff --git a/src/map.rs b/src/map.rs index 828ab92..d9855bf 100644 --- a/src/map.rs +++ b/src/map.rs @@ -17,6 +17,7 @@ pub struct Map { pub revealed_tiles: Vec, pub visible_tiles: Vec, pub blocked: Vec, + pub tile_content: Vec>, } impl Map { @@ -64,6 +65,7 @@ impl Map { revealed_tiles: vec![false; MAP_SIZE], visible_tiles: vec![false; MAP_SIZE], blocked: vec![false; MAP_SIZE], + tile_content: vec![Vec::new(); MAP_SIZE], }; const MAX_ROOMS: i32 = 30; @@ -125,6 +127,12 @@ impl Map { self.blocked[i] = *tile == TileType::Wall; } } + + pub fn clear_content_index(&mut self) { + for content in self.tile_content.iter_mut() { + content.clear(); + } + } } impl Algorithm2D for Map { diff --git a/src/map_indexing_system.rs b/src/map_indexing_system.rs index 4c8ba36..b64d8af 100644 --- a/src/map_indexing_system.rs +++ b/src/map_indexing_system.rs @@ -8,15 +8,26 @@ impl<'a> System<'a> for MapIndexingSystem { WriteExpect<'a, Map>, ReadStorage<'a, Position>, ReadStorage<'a, BlocksTile>, + Entities<'a>, ); fn run(&mut self, data: Self::SystemData) { - let (mut map, position, blockers) = data; + let (mut map, position, blockers, entities) = data; map.populate_blocked(); - for (position, _blocks) in (&position, &blockers).join() { + map.clear_content_index(); + + for (entity, position) in (&entities, &position).join() { let idx = map.xy_idx(position.x, position.y); - map.blocked[idx] = true; + + // If it's a blocking entity, note that in the map object + let _p: Option<&BlocksTile> = blockers.get(entity); + if let Some(_p) = _p { + map.blocked[idx] = true; + } + + // Push a copy of the entity to the indexed slot + map.tile_content[idx].push(entity); } } } diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs new file mode 100644 index 0000000..fd1dad9 --- /dev/null +++ b/src/melee_combat_system.rs @@ -0,0 +1,46 @@ +use crate::{CombatStats, Name, SufferDamage, WantsToMelee}; +use rltk::console; +use specs::prelude::*; + +pub struct MeleeCombatSystem {} + +impl<'a> System<'a> for MeleeCombatSystem { + type SystemData = ( + Entities<'a>, + WriteStorage<'a, WantsToMelee>, + ReadStorage<'a, Name>, + ReadStorage<'a, CombatStats>, + WriteStorage<'a, SufferDamage>, + ); + + fn run(&mut self, data: Self::SystemData) { + let (entities, mut wants_melee, names, combat_stats, mut inflict_damage) = data; + + for (_entity, wants_melee, name, stats) in + (&entities, &wants_melee, &names, &combat_stats).join() + { + if stats.hp > 0 { + let target_stats = combat_stats.get(wants_melee.target).unwrap(); + if target_stats.hp > 0 { + let target_name = names.get(wants_melee.target).unwrap(); + let damage = i32::max(0, stats.power - target_stats.defense); + + if damage == 0 { + console::log(&format!( + "{} is unable to hurt {}", + &name.name, &target_name.name + )); + } else { + console::log(&format!( + "{} hits {}, for {} hp", + &name.name, &target_name.name, damage + )); + SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage); + } + } + } + } + + wants_melee.clear(); + } +} diff --git a/src/monster_ai_system.rs b/src/monster_ai_system.rs index 68b4ec3..21fe9aa 100644 --- a/src/monster_ai_system.rs +++ b/src/monster_ai_system.rs @@ -1,5 +1,5 @@ -use super::{Map, Monster, Name, Position, Viewshed}; -use rltk::{console, Point}; +use crate::{Map, Monster, Position, RunState, Viewshed, WantsToMelee}; +use rltk::Point; use specs::prelude::*; pub struct MonsterAI {} @@ -9,27 +9,48 @@ impl<'a> System<'a> for MonsterAI { type SystemData = ( WriteExpect<'a, Map>, ReadExpect<'a, Point>, + ReadExpect<'a, Entity>, + ReadExpect<'a, RunState>, + Entities<'a>, WriteStorage<'a, Viewshed>, ReadStorage<'a, Monster>, - ReadStorage<'a, Name>, WriteStorage<'a, Position>, + WriteStorage<'a, WantsToMelee>, ); fn run(&mut self, data: Self::SystemData) { - let (mut map, player_pos, mut viewshed, monster, name, mut position) = data; + let ( + mut map, + player_pos, + player_entity, + runstate, + entities, + mut viewshed, + monster, + mut position, + mut wants_to_melee, + ) = data; - for (mut viewshed, _monster, name, mut pos) in - (&mut viewshed, &monster, &name, &mut position).join() + if *runstate != RunState::MonsterTurn { + return; + } + + for (entity, mut viewshed, _monster, mut pos) in + (&entities, &mut viewshed, &monster, &mut position).join() { let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos); if distance < 1.5 { // Attack goes here - console::log(&format!("{} shouts insults", name.name)); - return; - } - - if viewshed.visible_tiles.contains(&*player_pos) { + wants_to_melee + .insert( + entity, + WantsToMelee { + target: *player_entity, + }, + ) + .expect("Unable to insert attack"); + } else if viewshed.visible_tiles.contains(&*player_pos) { let path = rltk::a_star_search( map.xy_idx(pos.x, pos.y) as i32, map.xy_idx(player_pos.x, player_pos.y) as i32, @@ -37,9 +58,14 @@ impl<'a> System<'a> for MonsterAI { ); if path.success && path.steps.len() > 1 { + let mut idx = map.xy_idx(pos.x, pos.y); + + map.blocked[idx] = false; pos.x = path.steps[1] as i32 % map.width; pos.y = path.steps[1] as i32 / map.width; + idx = map.xy_idx(pos.x, pos.y); + map.blocked[idx] = true; viewshed.dirty = true; } } diff --git a/src/player.rs b/src/player.rs index e2a5dad..0b10fd8 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,26 +1,52 @@ -use super::{Map, Player, Position, State, TileType, Viewshed}; -use crate::RunState; +use crate::{CombatStats, Map, Player, Position, RunState, State, Viewshed, WantsToMelee}; use rltk::{Point, Rltk, VirtualKeyCode}; use specs::prelude::*; use std::cmp::{max, min}; pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) { let mut positions = ecs.write_storage::(); - let mut players = ecs.write_storage::(); + let players = ecs.read_storage::(); let mut viewsheds = ecs.write_storage::(); + let entities = ecs.entities(); + let combat_stats = ecs.read_storage::(); let map = ecs.fetch::(); + let mut wants_to_melee = ecs.write_storage::(); - for (_player, pos, viewshed) in (&mut players, &mut positions, &mut viewsheds).join() { + for (entity, _player, pos, viewshed) in + (&entities, &players, &mut positions, &mut viewsheds).join() + { + if pos.x + delta_x < 1 + || pos.x + delta_x > map.width - 1 + || pos.y + delta_y < 1 + || pos.y + delta_y > map.height - 1 + { + return; + } let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); + + for potential_target in map.tile_content[destination_idx].iter() { + let target = combat_stats.get(*potential_target); + if let Some(_target) = target { + wants_to_melee + .insert( + entity, + WantsToMelee { + target: *potential_target, + }, + ) + .expect("Add target failed"); + return; + } + } + if !map.blocked[destination_idx] { pos.x = min(79, max(0, pos.x + delta_x)); pos.y = min(49, max(0, pos.y + delta_y)); + viewshed.dirty = true; let mut ppos = ecs.write_resource::(); ppos.x = pos.x; ppos.y = pos.y; - - viewshed.dirty = true; } } } @@ -28,7 +54,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) { pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { // Player movement match ctx.key { - None => return RunState::Paused, // Nothing happened + None => return RunState::AwaitingInput, // Nothing happened Some(key) => match key { VirtualKeyCode::Left | VirtualKeyCode::Numpad4 @@ -65,9 +91,9 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { try_move_player(-1, 1, &mut gs.ecs) } - _ => return RunState::Paused, + _ => return RunState::AwaitingInput, }, } - RunState::Running + RunState::PlayerTurn }