From 3332d0a88d3e06b55d1d8ea7bdb633b7221f5d73 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 13 May 2021 12:25:45 -0500 Subject: [PATCH] Initial commit. --- .gitignore | 2 + Cargo.toml | 35 ++++ src/core.rs | 433 +++++++++++++++++++++++++++++++++++++++ src/error.rs | 56 +++++ src/exploration.rs | 386 ++++++++++++++++++++++++++++++++++ src/lib.rs | 21 ++ src/log.rs | 59 ++++++ src/map.rs | 370 +++++++++++++++++++++++++++++++++ src/navigation.rs | 500 +++++++++++++++++++++++++++++++++++++++++++++ src/pathfinding.rs | 219 ++++++++++++++++++++ src/sound.rs | 333 ++++++++++++++++++++++++++++++ src/visibility.rs | 324 +++++++++++++++++++++++++++++ 12 files changed, 2738 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/core.rs create mode 100644 src/error.rs create mode 100644 src/exploration.rs create mode 100644 src/lib.rs create mode 100644 src/log.rs create mode 100644 src/map.rs create mode 100644 src/navigation.rs create mode 100644 src/pathfinding.rs create mode 100644 src/sound.rs create mode 100644 src/visibility.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2f9e58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b7cf17b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "blackout" +version = "0.1.0" +authors = ["Nolan Darilek "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies.bevy] +version = "0.5" +default-features = false +features = [ + "bevy_gilrs", + "bevy_wgpu", + "bevy_winit", + "render", + "x11", + "wayland", + "serialize", +] + +[dependencies] +backtrace = "0.3" +bevy_input_actionmap = { path = "../bevy_input_actionmap" } +bevy_openal = { path = "../bevy_openal" } +bevy_tts = { path = "../bevy_tts" } +coord_2d = "0.3" +crossbeam-channel = "0.5" +derive_more = "0.99" +gilrs = "0.8" +mapgen = "0.4" +maze_generator = "1" +pathfinding = "2" +rand = "0.8" +shadowcast = "0.8" diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..dacdacf --- /dev/null +++ b/src/core.rs @@ -0,0 +1,433 @@ +use std::{ + cmp::{max, min}, + fmt::Display, +}; + +use bevy::{core::FloatOrd, prelude::*, transform::TransformSystem}; +use derive_more::{Deref, DerefMut}; + +#[derive(Clone, Copy, Debug, Default, Deref, DerefMut, PartialEq, PartialOrd, Reflect)] +#[reflect(Component)] +pub struct Coordinates(pub (f32, f32)); + +impl From<(f32, f32)> for Coordinates { + fn from(v: (f32, f32)) -> Self { + Coordinates((v.0, v.1)) + } +} + +impl From<(i32, i32)> for Coordinates { + fn from(v: (i32, i32)) -> Self { + Coordinates((v.0 as f32, v.1 as f32)) + } +} + +impl From<(u32, u32)> for Coordinates { + fn from(v: (u32, u32)) -> Self { + Coordinates((v.0 as f32, v.1 as f32)) + } +} + +impl From<(usize, usize)> for Coordinates { + fn from(v: (usize, usize)) -> Self { + Coordinates((v.0 as f32, v.1 as f32)) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Area { + pub rect: mapgen::geometry::Rect, + pub description: Option, +} + +impl Area { + pub fn contains(&self, point: &dyn PointLike) -> bool { + let x = point.x() as usize; + let y = point.y() as usize; + x >= self.rect.x1 && x <= self.rect.x2 && y >= self.rect.y1 && y <= self.rect.y2 + } + + pub fn center(&self) -> (usize, usize) { + let center = self.rect.center(); + (center.x, center.y) + } +} + +#[derive(Clone, Copy, Debug, Reflect)] +pub enum Angle { + Degrees(f32), + Radians(f32), +} + +impl Default for Angle { + fn default() -> Self { + Self::Radians(0.) + } +} + +impl Angle { + pub fn degrees(&self) -> f32 { + use Angle::*; + let mut degrees: f32 = match self { + Degrees(v) => *v, + Radians(v) => v.to_degrees(), + }; + while degrees < 0. { + degrees += 360.; + } + while degrees >= 360. { + degrees %= 360.; + } + degrees + } + + pub fn degrees_u32(&self) -> u32 { + self.degrees() as u32 + } + + pub fn radians(&self) -> f32 { + use Angle::*; + match self { + Degrees(v) => v.to_radians(), + Radians(v) => *v, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MovementDirection { + North, + NorthNortheast, + Northeast, + EastNortheast, + East, + EastSoutheast, + Southeast, + SouthSoutheast, + South, + SouthSouthwest, + Southwest, + WestSouthwest, + West, + WestNorthwest, + Northwest, + NorthNorthwest, +} + +impl MovementDirection { + pub fn new(heading: f32) -> Self { + use MovementDirection::*; + let mut heading = heading; + while heading >= 360. { + heading -= 360.; + } + while heading < 0. { + heading += 360.; + } + match heading { + h if h < 11.5 => East, + h if h < 34.0 => EastNortheast, + h if h < 56.5 => Northeast, + h if h < 79.0 => NorthNortheast, + h if h < 101.5 => North, + h if h < 124.0 => NorthNorthwest, + h if h < 146.5 => Northwest, + h if h < 169.0 => WestNorthwest, + h if h < 191.5 => West, + h if h < 214.0 => WestSouthwest, + h if h < 236.5 => Southwest, + h if h < 259.0 => SouthSouthwest, + h if h < 281.5 => South, + h if h < 304.0 => SouthSoutheast, + h if h < 326.5 => Southeast, + h if h <= 349.0 => EastSoutheast, + _ => East, + } + } +} + +impl From for MovementDirection { + fn from(angle: Angle) -> Self { + MovementDirection::new(angle.degrees()) + } +} + +// Converting from strings into directions doesn't make sense. +#[allow(clippy::from_over_into)] +impl Into for MovementDirection { + fn into(self) -> String { + use MovementDirection::*; + match self { + North => "north".to_string(), + NorthNortheast => "north northeast".to_string(), + Northeast => "northeast".to_string(), + EastNortheast => "east northeast".to_string(), + East => "east".to_string(), + EastSoutheast => "east southeast".to_string(), + Southeast => "southeast".to_string(), + SouthSoutheast => "south southeast".to_string(), + South => "south".to_string(), + SouthSouthwest => "south southwest".to_string(), + Southwest => "southwest".to_string(), + WestSouthwest => "west southwest".to_string(), + West => "west".to_string(), + WestNorthwest => "west northwest".to_string(), + Northwest => "northwest".to_string(), + NorthNorthwest => "north northwest".to_string(), + } + } +} + +impl Display for MovementDirection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str: String = (*self).into(); + write!(f, "{}", str) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CardinalDirection { + North, + East, + South, + West, +} + +impl CardinalDirection { + pub fn new(heading: f32) -> Self { + use CardinalDirection::*; + let mut heading = heading; + while heading >= 360. { + heading -= 360.; + } + while heading < 0. { + heading += 360.; + } + match heading { + h if h <= 45. => East, + h if h <= 135. => North, + h if h <= 225. => West, + h if h <= 315. => South, + _ => East, + } + } +} + +impl From for CardinalDirection { + fn from(angle: Angle) -> Self { + CardinalDirection::new(angle.degrees()) + } +} + +// Converting from strings into directions doesn't make sense. +#[allow(clippy::from_over_into)] +impl Into for CardinalDirection { + fn into(self) -> String { + use CardinalDirection::*; + match self { + North => "north".to_string(), + East => "east".to_string(), + South => "south".to_string(), + West => "west".to_string(), + } + } +} + +impl Display for CardinalDirection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str: String = (*self).into(); + write!(f, "{}", str) + } +} + +pub trait PointLike { + fn x(&self) -> f32; + + fn y(&self) -> f32; + + fn x_i32(&self) -> i32 { + self.x() as i32 + } + + fn y_i32(&self) -> i32 { + self.y() as i32 + } + + fn x_usize(&self) -> usize { + self.x() as usize + } + + fn y_usize(&self) -> usize { + self.y() as usize + } + fn i32(&self) -> (i32, i32) { + (self.x_i32(), self.y_i32()) + } + + fn to_index(&self, width: usize) -> usize { + ((self.y_i32() * width as i32) + self.x_i32()) as usize + } + + fn distance_squared(&self, other: &dyn PointLike) -> f32 { + let x1 = FloatOrd(self.x()); + let y1 = FloatOrd(self.y()); + let x2 = FloatOrd(other.x()); + let y2 = FloatOrd(other.y()); + let dx = max(x1, x2).0 - min(x1, x2).0; + let dy = max(y1, y2).0 - min(y1, y2).0; + (dx * dx) + (dy * dy) + } + + fn distance(&self, other: &dyn PointLike) -> f32 { + self.distance_squared(other).sqrt() + } + + fn bearing(&self, other: &dyn PointLike) -> f32 { + let y = other.y() - self.y(); + let x = other.x() - self.x(); + y.atan2(x) + } + + fn direction(&self, other: &dyn PointLike) -> MovementDirection { + let heading = self.bearing(other); + MovementDirection::new(heading.to_degrees()) + } + + fn distance_and_direction(&self, other: &dyn PointLike) -> String { + let mut tokens: Vec = vec![]; + let distance = self.distance(other).round() as i32; + if distance > 0 { + let tile_or_tiles = if distance == 1 { "tile" } else { "tiles" }; + let direction: String = self.direction(other).into(); + tokens.push(format!("{} {} {}", distance, tile_or_tiles, direction)); + } + tokens.join(" ") + } +} + +impl PointLike for (i32, i32) { + fn x(&self) -> f32 { + self.0 as f32 + } + + fn y(&self) -> f32 { + self.1 as f32 + } +} + +impl PointLike for (f32, f32) { + fn x(&self) -> f32 { + self.0 + } + + fn y(&self) -> f32 { + self.1 + } +} + +impl PointLike for (usize, usize) { + fn x(&self) -> f32 { + self.0 as f32 + } + + fn y(&self) -> f32 { + self.1 as f32 + } +} + +impl PointLike for &Coordinates { + fn x(&self) -> f32 { + self.0 .0 + } + + fn y(&self) -> f32 { + self.0 .1 + } +} + +impl PointLike for mapgen::geometry::Point { + fn x(&self) -> f32 { + self.x as f32 + } + + fn y(&self) -> f32 { + self.y as f32 + } +} + +#[macro_export] +macro_rules! impl_pointlike_for_tuple_component { + ($source:ty) => { + impl PointLike for $source { + fn x(&self) -> f32 { + self.0 .0 as f32 + } + + fn y(&self) -> f32 { + self.0 .1 as f32 + } + } + }; +} + +impl_pointlike_for_tuple_component!(Coordinates); + +impl From<&dyn PointLike> for (i32, i32) { + fn from(val: &dyn PointLike) -> Self { + (val.x_i32(), val.y_i32()) + } +} + +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct Player; + +fn copy_coordinates_to_transform( + config: Res, + mut query: Query<(&Coordinates, &mut Transform), Changed>, +) { + for (coordinates, mut transform) in query.iter_mut() { + transform.translation.x = coordinates.0 .0 * config.pixels_per_unit as f32; + transform.translation.y = coordinates.0 .1 * config.pixels_per_unit as f32; + } +} + +#[derive(Clone, Copy, Debug)] +pub struct CoreConfig { + pub pixels_per_unit: u8, +} + +impl Default for CoreConfig { + fn default() -> Self { + Self { pixels_per_unit: 1 } + } +} + +pub struct CorePlugin; + +impl Plugin for CorePlugin { + fn build(&self, app: &mut AppBuilder) { + if !app.world().contains_resource::() { + app.insert_resource(CoreConfig::default()); + } + app.register_type::() + .add_system(copy_coordinates_to_transform.system()) + .add_system_to_stage( + CoreStage::PostUpdate, + copy_coordinates_to_transform + .system() + .before(TransformSystem::TransformPropagate), + ); + } +} + +pub struct CorePlugins; + +impl PluginGroup for CorePlugins { + fn build(&mut self, group: &mut bevy::app::PluginGroupBuilder) { + group + .add(crate::bevy_tts::TtsPlugin) + .add(crate::bevy_openal::OpenAlPlugin) + .add(CorePlugin); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..c7d1f2a --- /dev/null +++ b/src/error.rs @@ -0,0 +1,56 @@ +use std::error::Error; +use std::{panic, thread}; + +use backtrace::Backtrace; +use bevy::prelude::*; + +pub fn error_handler(In(result): In>>) { + if let Err(e) = result { + error!("{}", e); + } +} + +fn init_panic_handler() { + panic::set_hook(Box::new(|info| { + let backtrace = Backtrace::default(); + + let thread = thread::current(); + let thread = thread.name().unwrap_or(""); + + let msg = match info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => match info.payload().downcast_ref::() { + Some(s) => &**s, + None => "Box", + }, + }; + + match info.location() { + Some(location) => { + error!( + target: "panic", "thread '{}' panicked at '{}': {}:{}{:?}", + thread, + msg, + location.file(), + location.line(), + backtrace + ); + } + None => error!( + target: "panic", + "thread '{}' panicked at '{}'{:?}", + thread, + msg, + backtrace + ), + } + })); +} + +pub struct ErrorPlugin; + +impl Plugin for ErrorPlugin { + fn build(&self, _: &mut AppBuilder) { + init_panic_handler(); + } +} diff --git a/src/exploration.rs b/src/exploration.rs new file mode 100644 index 0000000..fdcdaa7 --- /dev/null +++ b/src/exploration.rs @@ -0,0 +1,386 @@ +use std::error::Error; + +use bevy::prelude::*; +use bevy_input_actionmap::InputMap; +use bevy_tts::Tts; +use derive_more::{Deref, DerefMut}; +use mapgen::TileType; + +use crate::{ + core::{Coordinates, Player, PointLike}, + error::error_handler, + map::Map, + pathfinding::Destination, + visibility::{RevealedTiles, Viewshed, VisibleTiles}, +}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Reflect)] +#[reflect(Component)] +pub struct ExplorationFocused; + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Reflect)] +pub enum ExplorationType { + Exit = 0, + Item = 1, + Character = 2, + Ally = 3, + Enemy = 4, +} + +// Doesn't make sense to create from a `String`. +#[allow(clippy::from_over_into)] +impl Into for ExplorationType { + fn into(self) -> String { + match self { + ExplorationType::Exit => "Exit".into(), + ExplorationType::Item => "Item".into(), + ExplorationType::Character => "Character".into(), + ExplorationType::Ally => "Ally".into(), + ExplorationType::Enemy => "Enemy".into(), + } + } +} + +// Likewise. +#[allow(clippy::from_over_into)] +impl Into<&str> for ExplorationType { + fn into(self) -> &'static str { + match self { + ExplorationType::Exit => "exit", + ExplorationType::Item => "item", + ExplorationType::Character => "character", + ExplorationType::Ally => "ally", + ExplorationType::Enemy => "enemy", + } + } +} + +#[derive(Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)] +#[reflect(Component)] +pub struct Exploring(pub (f32, f32)); + +impl_pointlike_for_tuple_component!(Exploring); + +#[derive(Clone, Debug, Default, Deref, DerefMut)] +pub struct FocusedExplorationType(pub Option); + +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct Mappable; + +pub const ACTION_EXPLORE_FORWARD: &str = "explore_forward"; +pub const ACTION_EXPLORE_BACKWARD: &str = "explore_backward"; +pub const ACTION_EXPLORE_LEFT: &str = "explore_left"; +pub const ACTION_EXPLORE_RIGHT: &str = "explore_right"; +pub const ACTION_EXPLORE_FOCUS_NEXT: &str = "explore_focus_next"; +pub const ACTION_EXPLORE_FOCUS_PREV: &str = "explore_focus_prev"; +pub const ACTION_EXPLORE_SELECT_NEXT_TYPE: &str = "explore_select_next_type"; +pub const ACTION_EXPLORE_SELECT_PREV_TYPE: &str = "explore_select_prev_type"; +pub const ACTION_NAVIGATE_TO_EXPLORED: &str = "navigate_to"; + +fn exploration_type_change( + mut tts: ResMut, + input: Res>, + mut explorers: Query<(&Player, &Viewshed, &mut FocusedExplorationType)>, + features: Query<(&Coordinates, &ExplorationType)>, +) -> Result<(), Box> { + let changed = input.just_active(ACTION_EXPLORE_SELECT_NEXT_TYPE) + || input.just_active(ACTION_EXPLORE_SELECT_PREV_TYPE); + if !changed { + return Ok(()); + } + for (_, viewshed, mut focused) in explorers.iter_mut() { + let mut types: Vec = vec![]; + for (coordinates, t) in features.iter() { + let (x, y) = **coordinates; + let x = x as i32; + let y = y as i32; + if viewshed.visible.contains(&(x, y)) { + types.push(*t); + } + } + types.sort(); + types.dedup(); + if types.is_empty() { + tts.speak("Nothing visible.", true)?; + } else if input.just_active(ACTION_EXPLORE_SELECT_PREV_TYPE) { + if let Some(t) = &focused.0 { + if let Some(i) = types.iter().position(|v| *v == *t) { + if i == 0 { + focused.0 = None; + } else { + let t = &types[i - 1]; + focused.0 = Some(*t); + } + } else { + let t = types.last().unwrap(); + focused.0 = Some(*t); + } + } else { + let t = types.last().unwrap(); + focused.0 = Some(*t); + } + } else if input.just_active(ACTION_EXPLORE_SELECT_NEXT_TYPE) { + if let Some(t) = &focused.0 { + if let Some(i) = types.iter().position(|v| *v == *t) { + if i == types.len() - 1 { + focused.0 = None; + } else { + let t = &types[i + 1]; + focused.0 = Some(*t); + } + } else { + let t = types.first().unwrap(); + focused.0 = Some(*t); + } + } else { + let t = types.first().unwrap(); + focused.0 = Some(*t) + } + } + } + Ok(()) +} + +fn exploration_type_focus( + mut commands: Commands, + input: Res>, + mut tts: ResMut, + explorers: Query<( + Entity, + &Player, + &Viewshed, + &FocusedExplorationType, + Option<&Exploring>, + )>, + features: Query<(&Coordinates, &ExplorationType)>, +) -> Result<(), Box> { + let changed = input.just_active(ACTION_EXPLORE_FOCUS_NEXT) + || input.just_active(ACTION_EXPLORE_FOCUS_PREV); + if !changed { + return Ok(()); + } + for (entity, _, viewshed, focused, exploring) in explorers.iter() { + let mut features = features + .iter() + .filter(|(coordinates, _)| { + let (x, y) = ***coordinates; + let x = x as i32; + let y = y as i32; + viewshed.visible.contains(&(x, y)) + }) + .collect::>(); + features.sort_by(|(c1, _), (c2, _)| c1.partial_cmp(c2).unwrap()); + if let Some(focused) = &focused.0 { + features.retain(|(_, t)| **t == *focused); + } + if features.is_empty() { + tts.speak("Nothing visible.", true)?; + } else { + let mut target: Option<&(&Coordinates, &ExplorationType)> = None; + if input.just_active(ACTION_EXPLORE_FOCUS_NEXT) { + if let Some(exploring) = exploring { + target = features.iter().find(|(c, _)| ***c > **exploring); + if target.is_none() { + target = features.first(); + } + } else { + target = features.first(); + } + } else if input.just_active(ACTION_EXPLORE_FOCUS_PREV) { + if let Some(exploring) = exploring { + features.reverse(); + target = features.iter().find(|(c, _)| ***c < **exploring); + if target.is_none() { + target = features.first(); + } + } else { + target = features.last(); + } + } + if let Some((coordinates, _)) = target { + commands.entity(entity).insert(Exploring(***coordinates)); + } + } + } + Ok(()) +} + +fn exploration_type_changed_announcement( + mut tts: ResMut, + focused: Query< + ( + &FocusedExplorationType, + ChangeTrackers, + ), + Changed, + >, +) -> Result<(), Box> { + for (focused, changed) in focused.iter() { + if changed.is_added() { + return Ok(()); + } + match &focused.0 { + Some(v) => { + let v: String = (*v).into(); + tts.speak(v, true)?; + } + None => { + tts.speak("Everything", true)?; + } + }; + } + Ok(()) +} + +fn exploration_focus( + mut commands: Commands, + input: Res>, + map: Query<&Map>, + explorers: Query<(Entity, &Player, &Coordinates, Option<&Exploring>)>, +) { + for map in map.iter() { + for (entity, _, coordinates, exploring) in explorers.iter() { + let coordinates = **coordinates; + let coordinates = (coordinates.0.floor(), coordinates.1.floor()); + let mut exploring = if let Some(exploring) = exploring { + **exploring + } else { + coordinates + }; + let orig = exploring; + if input.just_active(ACTION_EXPLORE_FORWARD) { + exploring.1 += 1.; + } else if input.just_active(ACTION_EXPLORE_BACKWARD) { + exploring.1 -= 1.; + } else if input.just_active(ACTION_EXPLORE_LEFT) { + exploring.0 -= 1.; + } else if input.just_active(ACTION_EXPLORE_RIGHT) { + exploring.0 += 1.; + } + if orig != exploring + && exploring.0 >= 0. + && exploring.0 < map.width() as f32 + && exploring.1 >= 0. + && exploring.1 < map.height() as f32 + { + commands.entity(entity).insert(Exploring(exploring)); + } + } + } +} + +fn navigate_to_explored( + mut commands: Commands, + input: Res>, + map: Query<(&Map, &RevealedTiles)>, + explorers: Query<(Entity, &Exploring)>, +) { + for (entity, exploring) in explorers.iter() { + for (map, revealed_tiles) in map.iter() { + let point = **exploring; + let idx = point.to_index(map.width()); + let known = revealed_tiles[idx]; + if input.just_active(ACTION_NAVIGATE_TO_EXPLORED) && known { + commands + .entity(entity) + .insert(Destination((point.x_i32(), point.y_i32()))); + } + } + } +} + +fn exploration_changed_announcement( + mut commands: Commands, + mut tts: ResMut, + map: Query<(&Map, &RevealedTiles, &VisibleTiles)>, + explorers: Query<(&Coordinates, &Exploring), Changed>, + focused: Query<(Entity, &ExplorationFocused)>, + names: Query<&Name>, + types: Query<&ExplorationType>, + mappables: Query<&Mappable>, +) -> Result<(), Box> { + for (coordinates, exploring) in explorers.iter() { + let coordinates = **coordinates; + let coordinates = (coordinates.0.floor(), coordinates.1.floor()); + for (map, revealed_tiles, visible_tiles) in map.iter() { + let point = **exploring; + let idx = point.to_index(map.width()); + let known = revealed_tiles[idx]; + let visible = visible_tiles[idx]; + let fog_of_war = known && !visible; + let description = if known { + let mut tokens: Vec<&str> = vec![]; + for (entity, _) in focused.iter() { + commands.entity(entity).remove::(); + } + for entity in &map.entities[idx] { + commands + .entity(*entity) + .insert(ExplorationFocused::default()); + if visible || mappables.get(*entity).is_ok() { + if let Ok(name) = names.get(*entity) { + tokens.push(name.as_str()); + } + if tokens.is_empty() { + if let Ok(t) = types.get(*entity) { + tokens.push((*t).into()); + } + } + } + } + if tokens.is_empty() { + match map.base.tiles[idx] { + TileType::Floor => "Floor".to_string(), + TileType::Wall => "Wall".to_string(), + } + } else { + tokens.join(": ") + } + } else { + "Unknown".to_string() + }; + let mut tokens: Vec = vec![coordinates.distance_and_direction(exploring)]; + if fog_of_war { + tokens.push("in the fog of war".into()); + } + tts.speak(format!("{}: {}", description, tokens.join(", ")), true)?; + } + } + Ok(()) +} + +pub struct ExplorationPlugin; + +impl Plugin for ExplorationPlugin { + fn build(&self, app: &mut AppBuilder) { + app.register_type::() + .register_type::() + .register_type::() + .add_system(exploration_focus.system()) + .add_system( + exploration_type_focus + .system() + .chain(error_handler.system()), + ) + .add_system( + exploration_type_change + .system() + .chain(error_handler.system()), + ) + .add_system(navigate_to_explored.system()) + .add_system_to_stage( + CoreStage::PostUpdate, + exploration_type_changed_announcement + .system() + .chain(error_handler.system()), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + exploration_changed_announcement + .system() + .chain(error_handler.system()), + ); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2b14608 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,21 @@ +#![allow(clippy::too_many_arguments)] +#![allow(clippy::type_complexity)] + +pub use bevy_input_actionmap; +pub use bevy_openal; +pub use bevy_tts; +#[macro_use] +pub mod core; +pub use crossbeam_channel; +pub use derive_more; +pub mod error; +pub mod exploration; +pub use gilrs; +pub mod log; +pub mod map; +pub use mapgen; +pub mod navigation; +pub mod pathfinding; +pub use rand; +pub mod sound; +pub mod visibility; diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..877dd6d --- /dev/null +++ b/src/log.rs @@ -0,0 +1,59 @@ +use std::{error::Error, time::Instant}; + +use bevy::prelude::*; +use bevy_tts::Tts; +use derive_more::{Deref, DerefMut}; + +use crate::error::error_handler; + +#[derive(Clone, Debug, Default, Deref, DerefMut)] +pub struct Log(pub Vec); + +impl Log { + pub fn push>(&mut self, message: S) { + self.0.push(LogEntry { + time: Instant::now(), + message: message.into(), + }) + } +} + +#[derive(Clone, Debug)] +pub struct LogEntry { + pub time: Instant, + pub message: String, +} + +fn setup(mut commands: Commands) { + commands.spawn().insert(Log::default()); +} + +fn read_log( + mut tts: ResMut, + mut position: Local, + log: Query<&Log, Changed>, +) -> Result<(), Box> { + for log in log.iter() { + for (index, entry) in log.iter().enumerate() { + if index >= *position { + tts.speak(entry.message.clone(), false)?; + *position = index + 1; + } + } + } + Ok(()) +} + +pub struct LogPlugin; + +impl Plugin for LogPlugin { + fn build(&self, app: &mut AppBuilder) { + app.add_startup_system(setup.system()).add_system_to_stage( + CoreStage::PostUpdate, + read_log + .system() + .chain(error_handler.system()) + .after(crate::visibility::LOG_VISIBLE_LABEL), + ); + } +} diff --git a/src/map.rs b/src/map.rs new file mode 100644 index 0000000..57d80fb --- /dev/null +++ b/src/map.rs @@ -0,0 +1,370 @@ +use std::collections::{HashMap, HashSet}; + +use bevy::prelude::*; +use derive_more::{Deref, DerefMut}; +use mapgen::{geometry::Rect as MRect, Map as MapgenMap, MapFilter, TileType}; +use maze_generator::{prelude::*, recursive_backtracking::RbGenerator}; +use rand::prelude::StdRng; + +use crate::{ + core::{Area, Coordinates, Player, PointLike}, + exploration::{ExplorationType, Mappable}, + log::Log, +}; + +impl From for Coordinates { + fn from(point: mapgen::geometry::Point) -> Self { + Self((point.x as f32, point.y as f32)) + } +} +#[derive(Clone, Debug, Default, Deref, DerefMut)] +pub struct Areas(pub Vec); + +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct Exit; + +#[derive(Clone, Default)] +pub struct Map { + pub base: MapgenMap, + pub entities: Vec>, +} + +impl Map { + pub fn new(base: MapgenMap) -> Self { + let count = (base.width * base.height) as usize; + Self { + base, + entities: vec![HashSet::new(); count], + } + } + + pub fn width(&self) -> usize { + self.base.width + } + + pub fn height(&self) -> usize { + self.base.height + } + + pub fn count(&self) -> usize { + self.width() * self.height() + } + + pub fn start(&self) -> Option { + self.base.starting_point + } + + pub fn exit(&self) -> Option { + self.base.exit_point + } +} + +pub trait ITileType { + fn blocks_motion(&self) -> bool; + fn blocks_visibility(&self) -> bool; +} + +impl ITileType for TileType { + fn blocks_motion(&self) -> bool { + match self { + TileType::Wall => true, + TileType::Floor => false, + } + } + + fn blocks_visibility(&self) -> bool { + match self { + TileType::Wall => true, + TileType::Floor => false, + } + } +} + +#[derive(Clone, Debug)] +pub struct MapConfig { + pub autospawn_exits: bool, + pub speak_area_descriptions: bool, + pub start_revealed: bool, +} + +impl Default for MapConfig { + fn default() -> Self { + Self { + autospawn_exits: true, + speak_area_descriptions: true, + start_revealed: false, + } + } +} + +#[derive(Bundle)] +pub struct ExitBundle { + pub coordinates: Coordinates, + pub exit: Exit, + pub exploration_type: ExplorationType, + pub mappable: Mappable, + pub transform: Transform, + pub global_transform: GlobalTransform, +} + +impl Default for ExitBundle { + fn default() -> Self { + Self { + coordinates: Default::default(), + exit: Default::default(), + exploration_type: ExplorationType::Exit, + mappable: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + } + } +} + +#[derive(Bundle, Clone, Default)] +pub struct MapBundle { + pub map: Map, + pub children: Children, + pub transform: Transform, + pub global_transform: GlobalTransform, +} + +pub struct GridBuilder { + width_in_rooms: u32, + height_in_rooms: u32, + room_width: u32, + room_height: u32, +} + +impl GridBuilder { + pub fn new( + width_in_rooms: u32, + height_in_rooms: u32, + room_width: u32, + room_height: u32, + ) -> Box { + Box::new(GridBuilder { + width_in_rooms, + height_in_rooms, + room_width, + room_height, + }) + } +} + +impl MapFilter for GridBuilder { + fn modify_map(&self, _rng: &mut StdRng, map: &MapgenMap) -> MapgenMap { + let mut map = map.clone(); + let mut generator = RbGenerator::new(None); + let maze = generator.generate(self.width_in_rooms as i32, self.height_in_rooms as i32); + let total_height = (self.room_height + 1) * self.height_in_rooms + 1; + for y in 0..self.height_in_rooms { + for x in 0..self.width_in_rooms { + let x_offset = x * (self.room_width + 1); + let y_offset = total_height - (y * (self.room_height + 1)) - self.room_height - 2; + let room = MRect::new_i32( + x_offset as i32 + 1, + y_offset as i32 + 1, + self.room_width as i32, + self.room_height as i32, + ); + map.add_room(room); + let coords = maze_generator::prelude::Coordinates::new(x as i32, y as i32); + if let Some(field) = maze.get_field(&coords) { + let half_width = self.room_width / 2; + let half_height = self.room_height / 2; + use maze_generator::prelude::Direction::*; + if field.has_passage(&North) { + let x = x_offset + half_width; + let y = y_offset + self.room_height; + map.set_tile(x as usize, y as usize, TileType::Floor); + } + if field.has_passage(&South) { + let x = x_offset + half_width; + let y = y_offset; + map.set_tile(x as usize, y as usize, TileType::Floor); + } + if field.has_passage(&East) { + let x = x_offset + self.room_width; + let y = y_offset + half_height; + map.set_tile(x as usize, y as usize, TileType::Floor); + } + if field.has_passage(&West) { + let x = x_offset; + let y = y_offset + half_height; + map.set_tile(x as usize, y as usize, TileType::Floor); + } + } + } + } + map + } +} + +fn exit_spawner( + mut commands: Commands, + map: Query<(Entity, &Map), Added>, + config: Res, +) { + for (entity, map) in map.iter() { + if config.autospawn_exits { + let mut exits: Vec<(f32, f32)> = vec![]; + for x in 1..map.width() { + for y in 1..map.height() { + let mut spawn_exit = false; + if map.base.get_available_exits(x, y).len() > 2 { + let idx = (x, y).to_index(map.width()); + if map.base.tiles[idx] == TileType::Floor + && (x > 1 && map.base.tiles[idx - 1] == TileType::Floor) + && (x < map.width() - 2 && map.base.tiles[idx + 1] == TileType::Floor) + && (y > 1 + && map.base.tiles[idx - map.width() as usize] == TileType::Wall) + && (y < map.height() - 2 + && map.base.tiles[idx + map.width() as usize] == TileType::Wall) + { + spawn_exit = true; + } + if map.base.tiles[idx] == TileType::Floor + && (x > 1 && map.base.tiles[idx - 1] == TileType::Wall) + && (x < map.width() - 2 && map.base.tiles[idx + 1] == TileType::Wall) + && (y > 1 + && map.base.tiles[idx - map.width() as usize] == TileType::Floor) + && (y < map.height() - 2 + && map.base.tiles[idx + map.width() as usize] == TileType::Floor) + { + spawn_exit = true; + } + } + if spawn_exit { + let x = x as f32; + let y = y as f32; + if !exits.contains(&(x, y)) { + exits.push((x, y)); + } + } + } + } + for exit in exits { + let x = exit.0 as f32; + let y = exit.1 as f32; + let exit = commands + .spawn() + .insert_bundle(ExitBundle { + coordinates: Coordinates((x, y)), + exit: Default::default(), + transform: Transform::from_translation(Vec3::new(x, y, 0.)), + ..Default::default() + }) + .id(); + commands.entity(entity).push_children(&[exit]); + } + } + } +} + +fn area_description( + mut prev_area: Local>, + query: Query<(&Player, &Coordinates), Changed>, + map: Query<(&Map, &Areas)>, + mut log: Query<&mut Log>, +) { + for (_, coordinates) in query.iter() { + for (_, areas) in map.iter() { + let mut should_describe_area = false; + let mut current_area: Option = None; + for area in areas.iter() { + if area.contains(&*coordinates) { + current_area = Some(area.clone()); + if let Some(prev_area) = &*prev_area { + if prev_area != area { + should_describe_area = true; + } + } else { + should_describe_area = true; + } + break; + } + } + if should_describe_area { + if let Some(ref area) = current_area { + let description = if area.description.is_some() { + area.description.as_ref().unwrap().clone() + } else { + format!("{} by {} area.", area.rect.width(), area.rect.height()) + }; + for mut log in log.iter_mut() { + log.push(description.clone()); + } + } + } + *prev_area = current_area; + } + } +} + +#[derive(Default, Deref, DerefMut)] +struct PreviousIndex(HashMap); + +fn entity_indexing( + mut map: Query<&mut Map>, + mut previous_index: ResMut, + query: Query<(Entity, &Coordinates), Changed>, +) { + for (entity, coordinates) in query.iter() { + for mut map in map.iter_mut() { + let idx = coordinates.to_index(map.width()); + if let Some(prev_idx) = previous_index.get(&entity) { + if idx != *prev_idx { + map.entities[*prev_idx].retain(|&e| e != entity); + } + } + map.entities[idx].insert(entity); + previous_index.insert(entity, idx); + } + } +} + +fn add_areas(mut commands: Commands, query: Query<(Entity, &Map), (Added, Without)>) { + for (entity, map) in query.iter() { + let mut v = vec![]; + for room in &map.base.rooms { + v.push(Area { + rect: *room, + description: None, + }); + } + commands.entity(entity).insert(Areas(v)); + } +} + +pub const UPDATE_ENTITY_INDEX_LABEL: &str = "UPDATE_ENTITY_INDEX"; + +pub struct MapPlugin; + +impl Plugin for MapPlugin { + fn build(&self, app: &mut AppBuilder) { + if !app.world().contains_resource::() { + app.insert_resource(MapConfig::default()); + } + let config = app.world().get_resource::().unwrap().clone(); + const SPAWN_EXITS: &str = "SPAWN_EXITS"; + app.register_type::() + .insert_resource(PreviousIndex::default()) + .add_system(entity_indexing.system().label(UPDATE_ENTITY_INDEX_LABEL)) + .add_system( + exit_spawner + .system() + .label(SPAWN_EXITS) + .before(UPDATE_ENTITY_INDEX_LABEL), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + entity_indexing.system().label(UPDATE_ENTITY_INDEX_LABEL), + ) + .add_system_to_stage(CoreStage::Update, add_areas.system()) + .add_system_to_stage(CoreStage::PostUpdate, add_areas.system()); + if config.speak_area_descriptions { + app.add_system_to_stage(CoreStage::PostUpdate, area_description.system()); + } + } +} diff --git a/src/navigation.rs b/src/navigation.rs new file mode 100644 index 0000000..613c5d7 --- /dev/null +++ b/src/navigation.rs @@ -0,0 +1,500 @@ +use std::{collections::HashMap, error::Error, fmt::Debug, hash::Hash}; + +use bevy::prelude::*; +use bevy_input_actionmap::InputMap; +use bevy_tts::Tts; +use derive_more::{Deref, DerefMut}; + +use crate::{ + core::{Angle, CardinalDirection, Coordinates, Player, PointLike}, + error::error_handler, + exploration::{ExplorationFocused, Exploring}, + map::{ITileType, Map}, + pathfinding::Destination, +}; + +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct BlocksMotion; + +#[derive(Clone, Debug, Default, Deref, DerefMut, Reflect)] +#[reflect(Component)] +pub struct CollisionsMonitored(pub Vec); + +#[derive(Clone, Copy, Debug, Deref, DerefMut, Reflect)] +#[reflect(Component)] +pub struct MaxSpeed(pub f32); + +impl Default for MaxSpeed { + fn default() -> Self { + MaxSpeed(2.) + } +} + +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct MonitorsCollisions; + +#[derive(Clone, Debug, Default, Deref, DerefMut, Reflect)] +#[reflect(Component)] +pub struct MotionBlocked(pub Vec); + +#[derive(Clone, Copy, Debug, Deref, DerefMut, Reflect)] +#[reflect(Component)] +pub struct RotationSpeed(pub Angle); + +impl Default for RotationSpeed { + fn default() -> Self { + Self(Angle::Radians(0.)) + } +} + +#[derive(Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)] +#[reflect(Component)] +pub struct Speed(pub f32); + +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct Sprinting; + +#[derive(Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)] +#[reflect(Component)] +pub struct Velocity(pub Vec2); + +#[derive(Clone, Copy, Debug)] +pub struct Collision { + pub entity: Entity, + pub coordinates: (f32, f32), + pub index: usize, +} + +pub const ACTION_FORWARD: &str = "forward"; +pub const ACTION_BACKWARD: &str = "backward"; +pub const ACTION_LEFT: &str = "left"; +pub const ACTION_RIGHT: &str = "right"; +pub const ACTION_ROTATE_LEFT: &str = "ROTATE_LEFT"; +pub const ACTION_ROTATE_RIGHT: &str = "ROTATE_RIGHT"; +pub const ACTION_SPRINT: &str = "SPRINT"; + +fn movement_controls( + mut commands: Commands, + input: Res>, + time: Res