diff --git a/src/components.rs b/src/components.rs index abc6b5c..a595a11 100644 --- a/src/components.rs +++ b/src/components.rs @@ -35,3 +35,12 @@ pub struct Health { #[derive(Clone, PartialEq)] pub struct Name(pub String); + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ChasingPlayer; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Item; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct AmuletOfYala; diff --git a/src/main.rs b/src/main.rs index 093c305..e0e686d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,7 @@ impl State { let map_builder = MapBuilder::new(&mut rng); spawn_player(&mut ecs, map_builder.player_start); + spawn_amulet_of_yala(&mut ecs, map_builder.amulet_start); map_builder .rooms .iter() @@ -58,6 +59,71 @@ impl State { monster_systems: monster_build_scheduler(), } } + + 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 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); + } + + 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() + } + } } impl GameState for State { fn tick(&mut self, ctx: &mut BTerm) { @@ -82,6 +148,8 @@ impl GameState for State { 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"); } diff --git a/src/map.rs b/src/map.rs index de509ba..2031870 100644 --- a/src/map.rs +++ b/src/map.rs @@ -37,4 +37,51 @@ impl Map { Some(map_idx(point.x, point.y)) } } + fn valid_exit(&self, loc: Point, delta: Point) -> Option { + let destinaton = loc + delta; + if self.in_bounds(destinaton) { + if self.can_enter_tile(destinaton) { + let idx = self.point2d_to_index(destinaton); + Some(idx) + } else { + None + } + } else { + None + } + } +} + +impl Algorithm2D for Map { + fn dimensions(&self) -> Point { + Point::new(SCREEN_WIDTH, SCREEN_HEIGHT) + } + fn in_bounds(&self, pos: Point) -> bool { + self.in_bounds(pos) + } +} + +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 + } + fn get_pathing_distance(&self, idx1: usize, idx2: usize) -> f32 { + DistanceAlg::Pythagoras.distance2d(self.index_to_point2d(idx1), self.index_to_point2d(idx2)) + } } diff --git a/src/map_builder.rs b/src/map_builder.rs index df1c1d4..6a7a089 100644 --- a/src/map_builder.rs +++ b/src/map_builder.rs @@ -5,6 +5,7 @@ pub struct MapBuilder { pub map: Map, pub rooms: Vec, pub player_start: Point, + pub amulet_start: Point, } impl MapBuilder { @@ -13,11 +14,32 @@ impl 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( + SCREEN_WIDTH, + SCREEN_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 } diff --git a/src/spawners.rs b/src/spawners.rs index 8da6bd3..c28f48a 100644 --- a/src/spawners.rs +++ b/src/spawners.rs @@ -28,7 +28,7 @@ pub fn spawn_monster(ecs: &mut World, rng: &mut RandomNumberGenerator, pos: Poin color: ColorPair::new(WHITE, BLACK), glyph, }, - MovingRandomly {}, + ChasingPlayer {}, Health { current: hp, max: hp, @@ -43,3 +43,16 @@ fn goblin() -> (i32, String, FontCharType) { fn orc() -> (i32, String, FontCharType) { (2, "orc".to_string(), to_cp437('o')) } + +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()), + )); +} diff --git a/src/systems/chasing.rs b/src/systems/chasing.rs new file mode 100644 index 0000000..4ac52b1 --- /dev/null +++ b/src/systems/chasing.rs @@ -0,0 +1,60 @@ +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) { + let mut movers = <(Entity, &Point, &ChasingPlayer)>::query(); + let mut positions = <(Entity, &Point, &Health)>::query(); + let mut player = <(&Point, &Player)>::query(); + + let player_pos = player.iter(ecs).nth(0).unwrap().0; + let player_idx = map_idx(player_pos.x, player_pos.y); + + let search_targets = vec![player_idx]; + let dijkstra_map = DijkstraMap::new(SCREEN_WIDTH, SCREEN_HEIGHT, &search_targets, map, 1024.0); + + 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::() + .is_ok() + { + commands.push(( + (), + WantsToAttack { + attacker: *entity, + victim: *victim, + }, + )); + } + attacked = true; + }); + if !attacked { + commands.push(( + (), + WantsToMove { + entity: *entity, + destination, + }, + )); + } + } + }) +} diff --git a/src/systems/combat.rs b/src/systems/combat.rs index f30c886..66042b3 100644 --- a/src/systems/combat.rs +++ b/src/systems/combat.rs @@ -2,6 +2,7 @@ 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(); @@ -10,6 +11,11 @@ pub fn combat(ecs: &mut SubWorld, commands: &mut CommandBuffer) { .map(|(entity, attack)| (*entity, attack.victim)) .collect(); victims.iter().for_each(|(message, victim)| { + let is_player = ecs + .entry_ref(*victim) + .unwrap() + .get_component::() + .is_ok(); if let Ok(mut health) = ecs .entry_mut(*victim) .unwrap() @@ -17,7 +23,7 @@ pub fn combat(ecs: &mut SubWorld, commands: &mut CommandBuffer) { { println!("Health before attack: {}", health.current); health.current -= 1; - if health.current < 1 { + if health.current < 1 && !is_player { commands.remove(*victim); } println!("Health after attack: {}", health.current); diff --git a/src/systems/end_turn.rs b/src/systems/end_turn.rs index af8615e..04ac91a 100644 --- a/src/systems/end_turn.rs +++ b/src/systems/end_turn.rs @@ -1,11 +1,29 @@ use crate::prelude::*; #[system] -pub fn end_turn(#[resource] turn_state: &mut TurnState) { - let new_state = match turn_state { +#[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::()); + let mut amulet = <&Point>::query().filter(component::()); + let amulet_pos = amulet.iter(ecs).nth(0).unwrap(); + + let current_state = turn_state.clone(); + let mut new_state = match turn_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; } diff --git a/src/systems/mod.rs b/src/systems/mod.rs index c3cb65b..705ba1a 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +mod chasing; mod combat; mod end_turn; mod entity_render; @@ -34,6 +35,7 @@ pub fn player_build_scheduler() -> Schedule { pub fn monster_build_scheduler() -> Schedule { Schedule::builder() .add_system(random_move::random_move_system()) + .add_system(chasing::chasing_system()) .flush() .add_system(combat::combat_system()) .flush() diff --git a/src/turn_state.rs b/src/turn_state.rs index 2f8083f..e552be4 100644 --- a/src/turn_state.rs +++ b/src/turn_state.rs @@ -3,4 +3,6 @@ pub enum TurnState { AwaitingInput, PlayerTurn, MonsterTurn, + GameOver, + Victory, }