From 174ad7ea56616f4ed5534340c10356bf984802cf Mon Sep 17 00:00:00 2001 From: kjuulh Date: Tue, 25 Jan 2022 22:18:17 +0100 Subject: [PATCH] Add chapter 2.6 --- src/components.rs | 49 ++++++++ src/damage_system.rs | 45 +++++++ src/main.rs | 159 +++++++++++++++++++++++-- src/map.rs | 237 +++++++++++++++++++++++++++++-------- src/map_indexing_system.rs | 34 ++++++ src/melee_combat_system.rs | 48 ++++++++ src/monster_ai_system.rs | 75 ++++++++++++ src/player.rs | 62 +++++++--- src/rect.rs | 25 ++++ src/visibility_system.rs | 47 ++++++++ 10 files changed, 706 insertions(+), 75 deletions(-) create mode 100644 src/damage_system.rs create mode 100644 src/map_indexing_system.rs create mode 100644 src/melee_combat_system.rs create mode 100644 src/monster_ai_system.rs create mode 100644 src/rect.rs create mode 100644 src/visibility_system.rs diff --git a/src/components.rs b/src/components.rs index 794f6f9..88cc0d5 100644 --- a/src/components.rs +++ b/src/components.rs @@ -17,3 +17,52 @@ pub struct Renderable { #[derive(Component, Debug)] pub struct Player {} + +#[derive(Component)] +pub struct Viewshed { + pub visible_tiles: Vec, + pub range: i32, + pub dirty: bool, +} + +#[derive(Component)] +pub struct Monster {} + +#[derive(Component, Debug)] +pub struct Name { + pub name: String, +} + +#[derive(Component, Debug)] +pub struct BlocksTile {} + +#[derive(Component, Debug)] +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..a18532e --- /dev/null +++ b/src/damage_system.rs @@ -0,0 +1,45 @@ +use rltk::console; +use specs::prelude::*; + +use crate::components::{CombatStats, Player, SufferDamage}; + +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(); + { + 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"); + } +} diff --git a/src/main.rs b/src/main.rs index cf7cda0..ae9bda5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,54 @@ use components::*; +use damage_system::DamageSystem; use map::*; +use map_indexing_system::MapIndexingSystem; +use melee_combat_system::MeleeCombatSystem; +use monster_ai_system::*; use player::*; -use rltk::{GameState, RGB}; +use rltk::{GameState, Point, RGB}; +use visibility_system::*; use specs::prelude::*; mod components; +mod damage_system; mod map; +mod map_indexing_system; +mod melee_combat_system; +mod monster_ai_system; mod player; +mod rect; +mod visibility_system; + +#[derive(PartialEq, Copy, Clone)] +pub enum RunState { + AwaitingInput, + PreRun, + PlayerTurn, + MonsterTurn, +} pub struct State { - ecs: World, + pub ecs: World, } impl State { fn run_systems(&mut self) { + let mut vis = VisibilitySystem {}; + vis.run_now(&self.ecs); + + let mut mob = MonsterAI {}; + mob.run_now(&self.ecs); + + let mut mapindex = MapIndexingSystem {}; + mapindex.run_now(&self.ecs); + + let mut meleesystem = MeleeCombatSystem {}; + meleesystem.run_now(&self.ecs); + + let mut damagesystem = DamageSystem {}; + damagesystem.run_now(&self.ecs); + self.ecs.maintain(); } } @@ -23,17 +57,45 @@ impl GameState for State { fn tick(&mut self, ctx: &mut rltk::Rltk) { ctx.cls(); - player_input(self, ctx); - self.run_systems(); + let mut newrunstate; + { + let runstate = self.ecs.fetch::(); + newrunstate = *runstate; + } - let map = self.ecs.fetch::>(); - draw_map(&map, ctx); + 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::(); let renderables = self.ecs.read_storage::(); + let map = self.ecs.fetch::(); for (pos, render) in (&positions, &renderables).join() { - ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) + 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) + } } } } @@ -47,21 +109,98 @@ fn main() -> rltk::BError { .build()?; let mut gs = State { ecs: World::new() }; + gs.ecs.insert(RunState::PreRun); + gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); - gs.ecs.insert(new_map_rooms_and_corridors()); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + let map = Map::new_map_rooms_and_corridors(); + let (player_x, player_y) = map.rooms[0].center(); - gs.ecs + gs.ecs.insert(Point::new(player_x, player_y)); + let player_entity = gs + .ecs .create_entity() - .with(Position { x: 40, y: 25 }) + .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(); + gs.ecs.insert(player_entity); + + let mut rng = rltk::RandomNumberGenerator::new(); + for (i, room) in map.rooms.iter().skip(1).enumerate() { + let (x, y) = room.center(); + let glyph: rltk::FontCharType; + let roll = rng.roll_dice(1, 2); + let name: String; + match roll { + 1 => { + glyph = rltk::to_cp437('g'); + name = "Goblin".to_string(); + } + _ => { + glyph = rltk::to_cp437('o'); + name = "Orc".to_string(); + } + } + + gs.ecs + .create_entity() + .with(Position { x, y }) + .with(Renderable { + glyph, + fg: RGB::named(rltk::RED), + bg: RGB::named(rltk::BLACK), + }) + .with(Viewshed { + visible_tiles: Vec::new(), + range: 8, + dirty: true, + }) + .with(Monster {}) + .with(Name { + name: format!("{} #{}", &name, i), + }) + .with(BlocksTile {}) + .with(CombatStats { + max_hp: 16, + hp: 16, + defense: 1, + power: 4, + }) + .build(); + } + + gs.ecs.insert(map); + rltk::main_loop(context, gs) } diff --git a/src/map.rs b/src/map.rs index e65a63f..e1a0e7d 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,4 +1,9 @@ -use rltk::{Rltk, RGB}; +use std::cmp::{max, min}; + +use rltk::{Algorithm2D, BaseMap, Point, RandomNumberGenerator, Rltk, RGB}; +use specs::prelude::*; + +use crate::rect::Rect; #[derive(PartialEq, Copy, Clone)] pub enum TileType { @@ -6,68 +11,200 @@ pub enum TileType { Floor, } -pub fn xy_idx(x: i32, y: i32) -> usize { - (y as usize * 80) + x as usize +pub struct Map { + pub tiles: Vec, + pub rooms: Vec, + pub width: i32, + pub height: i32, + pub revealed_tiles: Vec, + pub visible_tiles: Vec, + pub blocked: Vec, + pub tile_content: Vec>, } -pub fn new_map_rooms_and_corridors() -> Vec { - let mut map = vec![TileType::Wall; 80 * 50]; - - map -} - -/// Makes a map with solid boundaries and 400 randomly placed walls. No guarantees that it won't look awful -pub fn new_map_test() -> Vec { - let mut map = vec![TileType::Floor; 80 * 50]; - - for x in 0..80 { - map[xy_idx(x, 0)] = TileType::Wall; - map[xy_idx(x, 49)] = TileType::Wall; +impl Map { + pub fn xy_idx(&self, x: i32, y: i32) -> usize { + (y as usize * self.width as usize) + x as usize } - for y in 0..50 { - map[xy_idx(0, y)] = TileType::Wall; - map[xy_idx(79, y)] = TileType::Wall; - } - - let mut rng = rltk::RandomNumberGenerator::new(); - - for _i in 0..400 { - let x = rng.roll_dice(1, 79); - let y = rng.roll_dice(1, 49); - let idx = xy_idx(x, y); - if idx != xy_idx(40, 25) { - map[idx] = TileType::Wall; + fn apply_room_to_map(&mut self, room: &Rect) { + for y in room.y1 + 1..=room.y2 { + for x in room.x1 + 1..=room.x2 { + let idx = self.xy_idx(x, y); + self.tiles[idx] = TileType::Floor; + } } } - map + fn apply_horizontal_tunnel(&mut self, x1: i32, x2: i32, y: i32) { + for x in min(x1, x2)..=max(x1, x2) { + let idx = self.xy_idx(x, y); + if idx > 0 && idx < self.width as usize * self.height as usize { + self.tiles[idx as usize] = TileType::Floor; + } + } + } + + fn apply_vertical_tunnel(&mut self, y1: i32, y2: i32, x: i32) { + for y in min(y1, y2)..=max(y1, y2) { + let idx = self.xy_idx(x, y); + if idx > 0 && idx < self.width as usize * self.height as usize { + self.tiles[idx as usize] = TileType::Floor; + } + } + } + + pub fn new_map_rooms_and_corridors() -> Map { + let mut map = Map { + tiles: vec![TileType::Wall; 80 * 50], + rooms: Vec::new(), + width: 80, + height: 50, + revealed_tiles: vec![false; 80 * 50], + visible_tiles: vec![false; 80 * 50], + blocked: vec![false; 80 * 50], + tile_content: vec![Vec::new(); 80 * 50], + }; + + const MAX_ROOMS: i32 = 30; + const MIN_SIZE: i32 = 6; + const MAX_SIZE: i32 = 10; + + let mut rng = RandomNumberGenerator::new(); + + for _ in 0..MAX_ROOMS { + let w = rng.range(MIN_SIZE, MAX_SIZE); + let h = rng.range(MIN_SIZE, MAX_SIZE); + let x = rng.roll_dice(1, map.width - w - 1) - 1; + let y = rng.roll_dice(1, map.height - h - 1) - 1; + let new_room = Rect::new(x, y, w, h); + let mut ok = true; + for other_room in map.rooms.iter() { + if new_room.intersect(other_room) { + ok = false + }; + } + if ok { + map.apply_room_to_map(&new_room); + + if !map.rooms.is_empty() { + let (new_x, new_y) = new_room.center(); + let (prev_x, prev_y) = map.rooms[map.rooms.len() - 1].center(); + if rng.range(0, 2) == 1 { + map.apply_horizontal_tunnel(prev_x, new_x, prev_y); + map.apply_vertical_tunnel(prev_y, new_y, new_x); + } else { + map.apply_vertical_tunnel(prev_y, new_y, prev_x); + map.apply_horizontal_tunnel(prev_x, new_x, new_y); + } + } + + map.rooms.push(new_room); + } + } + + map + } + + fn is_exit_valid(&self, x: i32, y: i32) -> bool { + if x < 1 || x > self.width - 1 || y < 1 || y > self.height - 1 { + return false; + } + let idx = self.xy_idx(x, y); + !self.blocked[idx] + } + + pub fn populate_blocked(&mut self) { + for (i, tile) in self.tiles.iter_mut().enumerate() { + self.blocked[i] = *tile == TileType::Wall; + } + } + + pub fn clear_content_index(&mut self) { + for content in self.tile_content.iter_mut() { + content.clear(); + } + } } -pub fn draw_map(map: &[TileType], ctx: &mut Rltk) { +impl Algorithm2D for Map { + fn dimensions(&self) -> rltk::Point { + Point::new(self.width, self.height) + } +} + +impl BaseMap for Map { + fn is_opaque(&self, idx: usize) -> bool { + self.tiles[idx as usize] == TileType::Wall + } + + fn get_pathing_distance(&self, idx1: usize, idx2: usize) -> f32 { + let w = self.width as usize; + let p1 = Point::new(idx1 % w, idx1 / w); + let p2 = Point::new(idx2 % w, idx2 / w); + rltk::DistanceAlg::Pythagoras.distance2d(p1, p2) + } + + fn get_available_exits(&self, idx: usize) -> rltk::SmallVec<[(usize, f32); 10]> { + let mut exits = rltk::SmallVec::new(); + let x = idx as i32 % self.width; + let y = idx as i32 / self.width; + let w = self.width as usize; + + if self.is_exit_valid(x - 1, y) { + exits.push((idx - 1, 1.0)) + }; + if self.is_exit_valid(x + 1, y) { + exits.push((idx + 1, 1.0)) + }; + if self.is_exit_valid(x, y - 1) { + exits.push((idx - w, 1.0)) + }; + if self.is_exit_valid(x, y + 1) { + exits.push((idx + w, 1.0)) + }; + + if self.is_exit_valid(x - 1, y - 1) { + exits.push((idx - w - 1, 1.45)) + }; + if self.is_exit_valid(x + 1, y - 1) { + exits.push((idx - w + 1, 1.45)) + }; + if self.is_exit_valid(x - 1, y + 1) { + exits.push((idx + w - 1, 1.45)) + }; + if self.is_exit_valid(x + 1, y + 1) { + exits.push((idx + w + 1, 1.45)) + }; + + exits + } +} + +pub fn draw_map(ecs: &World, ctx: &mut Rltk) { + let map = ecs.fetch::(); + let mut y = 0; let mut x = 0; - for tile in map.iter() { - match tile { - TileType::Floor => { - ctx.set( - x, - y, - RGB::from_f32(0.5, 0.5, 0.5), - RGB::from_f32(0., 0., 0.), - rltk::to_cp437('.'), - ); - } + for (idx, tile) in map.tiles.iter().enumerate() { + if map.revealed_tiles[idx] { + let glyph; + let mut fg; + match tile { + TileType::Floor => { + fg = RGB::from_f32(0.5, 0.5, 0.5); + glyph = rltk::to_cp437('.'); + } - TileType::Wall => { - ctx.set( - x, - y, - RGB::from_f32(0.0, 1.0, 0.0), - RGB::from_f32(0., 0., 0.), - rltk::to_cp437('#'), - ); + TileType::Wall => { + fg = RGB::from_f32(0.0, 1.0, 0.0); + glyph = rltk::to_cp437('#'); + } } + if !map.visible_tiles[idx] { + fg = fg.to_greyscale() + } + ctx.set(x, y, fg, RGB::from_f32(0., 0., 0.), glyph); } x += 1; diff --git a/src/map_indexing_system.rs b/src/map_indexing_system.rs new file mode 100644 index 0000000..108b6ff --- /dev/null +++ b/src/map_indexing_system.rs @@ -0,0 +1,34 @@ +use specs::prelude::*; + +use crate::{ + components::{BlocksTile, Position}, + map::Map, +}; + +pub struct MapIndexingSystem {} + +impl<'a> System<'a> for MapIndexingSystem { + type SystemData = ( + WriteExpect<'a, Map>, + ReadStorage<'a, Position>, + ReadStorage<'a, BlocksTile>, + Entities<'a>, + ); + + fn run(&mut self, data: Self::SystemData) { + let (mut map, position, blockers, entities) = data; + + map.populate_blocked(); + map.clear_content_index(); + for (entity, position) in (&entities, &position).join() { + let idx = map.xy_idx(position.x, position.y); + + let _p: Option<&BlocksTile> = blockers.get(entity); + if let Some(_p) = _p { + map.blocked[idx] = true; + } + + 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..84be607 --- /dev/null +++ b/src/melee_combat_system.rs @@ -0,0 +1,48 @@ +use rltk::console; +use specs::prelude::*; + +use crate::components::{CombatStats, Name, SufferDamage, WantsToMelee}; + +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 new file mode 100644 index 0000000..02b3793 --- /dev/null +++ b/src/monster_ai_system.rs @@ -0,0 +1,75 @@ +use rltk::{console, Point}; +use specs::prelude::*; + +use crate::{ + components::{Monster, Position, Viewshed, WantsToMelee}, + map::Map, + RunState, +}; + +pub struct MonsterAI {} + +impl<'a> System<'a> for MonsterAI { + #[allow(clippy::type_complexity)] + type SystemData = ( + WriteExpect<'a, Map>, + ReadExpect<'a, Point>, + ReadExpect<'a, Entity>, + ReadExpect<'a, RunState>, + Entities<'a>, + WriteStorage<'a, Viewshed>, + ReadStorage<'a, Monster>, + WriteStorage<'a, Position>, + WriteStorage<'a, WantsToMelee>, + ); + + fn run(&mut self, data: Self::SystemData) { + let ( + mut map, + player_pos, + player_entity, + runstate, + entities, + mut viewshed, + monster, + mut position, + mut wants_to_melee, + ) = data; + + 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 { + 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, + &mut *map, + ); + 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 95300cc..1fa0a46 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,35 +1,67 @@ use crate::{ - components::{Player, Position}, - map::{xy_idx, TileType}, - State, + components::{CombatStats, Player, Position, Viewshed, WantsToMelee}, + map::{Map, TileType}, + RunState, State, }; -use rltk::Rltk; +use rltk::{console, 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 map = ecs.fetch::>(); + let mut viewsheds = ecs.write_storage::(); + let combat_stats = ecs.read_storage::(); + let entities = ecs.entities(); + let mut wants_to_melee = ecs.write_storage::(); + let map = ecs.fetch::(); - for (_player, pos) in (&mut players, &mut positions).join() { - let destination_idx = xy_idx(pos.x + delta_x, pos.y + delta_y); - if map[destination_idx] != TileType::Wall { + for (entity, _player, pos, viewshed) in + (&entities, &mut players, &mut positions, &mut viewsheds).join() + { + 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"); + } + } + + 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; } } } -pub fn player_input(gs: &mut State, ctx: &mut Rltk) { +pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { match ctx.key { - None => {} + None => return RunState::AwaitingInput, Some(key) => match key { - rltk::VirtualKeyCode::Left => try_move_player(-1, 0, &mut gs.ecs), - rltk::VirtualKeyCode::Right => try_move_player(1, 0, &mut gs.ecs), - rltk::VirtualKeyCode::Up => try_move_player(0, -1, &mut gs.ecs), - rltk::VirtualKeyCode::Down => try_move_player(0, 1, &mut gs.ecs), - _ => {} + VirtualKeyCode::Left | VirtualKeyCode::H => try_move_player(-1, 0, &mut gs.ecs), + VirtualKeyCode::Right | VirtualKeyCode::L => try_move_player(1, 0, &mut gs.ecs), + VirtualKeyCode::Up | VirtualKeyCode::K => try_move_player(0, -1, &mut gs.ecs), + VirtualKeyCode::Down | VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs), + VirtualKeyCode::Y => try_move_player(1, -1, &mut gs.ecs), + VirtualKeyCode::U => try_move_player(-1, -1, &mut gs.ecs), + VirtualKeyCode::N => try_move_player(1, 1, &mut gs.ecs), + VirtualKeyCode::B => try_move_player(-1, 1, &mut gs.ecs), + _ => return RunState::AwaitingInput, }, } + + RunState::PlayerTurn } diff --git a/src/rect.rs b/src/rect.rs new file mode 100644 index 0000000..3b74c16 --- /dev/null +++ b/src/rect.rs @@ -0,0 +1,25 @@ +pub struct Rect { + pub x1: i32, + pub x2: i32, + pub y1: i32, + pub y2: i32, +} + +impl Rect { + pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self { + Self { + x1: x, + y1: y, + x2: x + w, + y2: y + h, + } + } + + pub fn intersect(&self, other: &Rect) -> bool { + self.x1 <= other.x2 && self.x2 >= other.x1 && self.y1 <= other.y2 && self.y2 >= other.y1 + } + + pub fn center(&self) -> (i32, i32) { + ((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2) + } +} diff --git a/src/visibility_system.rs b/src/visibility_system.rs new file mode 100644 index 0000000..a6a9cd4 --- /dev/null +++ b/src/visibility_system.rs @@ -0,0 +1,47 @@ +use rltk::{field_of_view, Point}; +use specs::prelude::*; + +use crate::{ + components::{Player, Position, Viewshed}, + map::Map, +}; + +pub struct VisibilitySystem {} + +impl<'a> System<'a> for VisibilitySystem { + type SystemData = ( + WriteExpect<'a, Map>, + Entities<'a>, + WriteStorage<'a, Viewshed>, + WriteStorage<'a, Position>, + ReadStorage<'a, Player>, + ); + + fn run(&mut self, data: Self::SystemData) { + let (mut map, entities, mut viewshed, pos, player) = data; + for (ent, viewshed, pos) in (&entities, &mut viewshed, &pos).join() { + if !viewshed.dirty { + return; + } + + viewshed.dirty = false; + viewshed.visible_tiles.clear(); + viewshed.visible_tiles = field_of_view(Point::new(pos.x, pos.y), viewshed.range, &*map); + viewshed + .visible_tiles + .retain(|p| p.x >= 0 && p.x < map.width && p.y >= 0 && p.y < map.height); + + let p: Option<&Player> = player.get(ent); + if let Some(_p) = p { + for t in map.visible_tiles.iter_mut() { + *t = false + } + for vis in viewshed.visible_tiles.iter() { + let idx = map.xy_idx(vis.x, vis.y); + map.revealed_tiles[idx] = true; + map.visible_tiles[idx] = true; + } + } + } + } +}