use std::{ cmp::{max, min}, f32::consts::PI, fmt::Display, marker::PhantomData, ops::Sub, sync::RwLock, }; use bevy::{ecs::query::WorldQuery, prelude::*, utils::FloatOrd}; use bevy_rapier2d::{ parry::query::{closest_points, distance, ClosestPoints}, prelude::*, rapier::{math::Isometry, prelude::AABB}, }; use once_cell::sync::Lazy; use rand::prelude::*; use serde::{Deserialize, Serialize}; #[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, } } fn relative_desc(&self) -> String { let mode = RELATIVE_DIRECTION_MODE.read().unwrap(); match self.degrees() { v if v <= 15. => "ahead", v if v <= 45. => { if *mode == RelativeDirectionMode::ClockFacing { "11:00" } else { "ahead and left" } } v if v <= 75. => { if *mode == RelativeDirectionMode::ClockFacing { "10:00" } else { "left and ahead" } } v if v <= 105. => "left", v if v <= 135. => { if *mode == RelativeDirectionMode::ClockFacing { "8:00" } else { "left and behind" } } v if v <= 165. => { if *mode == RelativeDirectionMode::ClockFacing { "7:00" } else { "behind and left" } } v if v <= 195. => "behind", v if v <= 225. => { if *mode == RelativeDirectionMode::ClockFacing { "5:00" } else { "behind and right" } } v if v <= 255. => { if *mode == RelativeDirectionMode::ClockFacing { "4:00" } else { "right and behind" } } v if v <= 285. => "right", v if v <= 315. => { if *mode == RelativeDirectionMode::ClockFacing { "2:00" } else { "right and ahead" } } v if v <= 345. => { if *mode == RelativeDirectionMode::ClockFacing { "1:00" } else { "ahead and right" } } _ => "ahead", } .into() } } impl Sub for Angle { type Output = Self; fn sub(self, rhs: Self) -> Self::Output { match self { Angle::Degrees(v1) => match rhs { Angle::Degrees(v2) => Angle::Degrees(v1 - v2), Angle::Radians(v2) => Angle::Degrees(v1 - v2.to_degrees()), }, Angle::Radians(v1) => match rhs { Angle::Degrees(v2) => Angle::Radians(v1 - v2.to_radians()), Angle::Radians(v2) => Angle::Radians(v1 - v2), }, } } } #[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 From for MovementDirection { fn from(angle: Angle) -> Self { let heading = angle.degrees(); use MovementDirection::*; 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, } } } // 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(Component, Clone, Copy, Debug, Eq, PartialEq)] pub enum CardinalDirection { North, East, South, West, } impl From for CardinalDirection { fn from(angle: Angle) -> Self { let heading = angle.degrees(); use CardinalDirection::*; 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<&CardinalDirection> for Angle { fn from(direction: &CardinalDirection) -> Self { use CardinalDirection::*; match direction { North => Angle::Radians(PI / 2.), East => Angle::Radians(0.), South => Angle::Radians(PI * 1.5), West => Angle::Radians(PI), } } } // 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) } } #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum RelativeDirectionMode { ClockFacing, Directional, } static RELATIVE_DIRECTION_MODE: Lazy> = Lazy::new(|| { let v = RelativeDirectionMode::Directional; RwLock::new(v) }); static PHYSICS_SCALE: Lazy> = Lazy::new(|| RwLock::new(1.)); pub trait PointLike { fn x(&self) -> f32; fn y(&self) -> f32; fn x_i32(&self) -> i32 { self.x().trunc() as i32 } fn y_i32(&self) -> i32 { self.y().trunc() as i32 } fn x_usize(&self) -> usize { self.x().trunc() as usize } fn y_usize(&self) -> usize { self.y().trunc() as usize } fn f32(&self) -> (f32, f32) { (self.x(), self.y()) } fn i32(&self) -> (i32, i32) { (self.x_i32(), self.y_i32()) } fn floor(&self) -> (f32, f32) { (self.x().floor(), self.y().floor()) } 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) -> Angle { let y = other.y() - self.y(); let x = other.x() - self.x(); Angle::Radians(y.atan2(x)) } fn direction(&self, other: &dyn PointLike) -> MovementDirection { self.bearing(other).into() } fn direction_and_distance(&self, other: &dyn PointLike, yaw: Option) -> 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 = if let Some(yaw) = yaw { let bearing = self.bearing(other); (bearing - yaw).relative_desc() } else { self.direction(other).into() }; tokens.push(format!("{direction} {distance} {tile_or_tiles}")); } tokens.join(" ") } } impl PointLike for Transform { fn x(&self) -> f32 { self.translation.x } fn y(&self) -> f32 { self.translation.y } } impl PointLike for &Transform { fn x(&self) -> f32 { self.translation.x } fn y(&self) -> f32 { self.translation.y } } impl PointLike for GlobalTransform { fn x(&self) -> f32 { self.translation().x } fn y(&self) -> f32 { self.translation().y } } impl PointLike for &GlobalTransform { fn x(&self) -> f32 { self.translation().x } fn y(&self) -> f32 { self.translation().y } } impl PointLike for Vec2 { fn x(&self) -> f32 { self.x } fn y(&self) -> f32 { self.y } } 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 here_be_dragons::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 From<&dyn PointLike> for (i32, i32) { fn from(val: &dyn PointLike) -> Self { val.i32() } } pub trait TransformExt { fn yaw(&self) -> Angle; } impl TransformExt for Transform { fn yaw(&self) -> Angle { let forward = self.right(); Angle::Radians(forward.y.atan2(forward.x)) } } pub trait GlobalTransformExt { fn yaw(&self) -> Angle; fn closest_points( &self, collider: &Collider, other: &GlobalTransform, other_collider: &Collider, ) -> ClosestPoints; fn collider_direction_and_distance( &self, collider: &Collider, other: &GlobalTransform, other_collider: &Collider, ) -> String; } impl GlobalTransformExt for GlobalTransform { fn yaw(&self) -> Angle { let forward = self.right(); Angle::Radians(forward.y.atan2(forward.x)) } fn closest_points( &self, collider: &Collider, other: &GlobalTransform, other_collider: &Collider, ) -> ClosestPoints { use bevy::math::Vec3Swizzles; let scale = PHYSICS_SCALE.read().unwrap(); let pos1 = Isometry::new( (self.translation() / *scale).xy().into(), self.yaw().radians(), ); let pos2 = Isometry::new( (other.translation() / *scale).xy().into(), other.yaw().radians(), ); closest_points(&pos1, &*collider.raw, &pos2, &*other_collider.raw, f32::MAX).unwrap() } fn collider_direction_and_distance( &self, collider: &Collider, other: &GlobalTransform, other_collider: &Collider, ) -> String { use bevy::math::Vec3Swizzles; let scale = PHYSICS_SCALE.read().unwrap(); let pos1 = Isometry::new( (self.translation() / *scale).xy().into(), self.yaw().radians(), ); let pos2 = Isometry::new( (other.translation() / *scale).xy().into(), other.yaw().radians(), ); let closest = self.closest_points(collider, other, other_collider); let distance = distance(&pos1, &*collider.raw, &pos2, &*other_collider.raw).unwrap() as u32; let tile_or_tiles = if distance == 1 { "tile" } else { "tiles" }; if distance > 0 { if let ClosestPoints::WithinMargin(p1, p2) = closest { let p1 = (p1.x, p1.y); let p2 = (p2.x, p2.y); let bearing = p1.bearing(&p2); let yaw = self.yaw(); let direction = (bearing - yaw).relative_desc(); format!("{direction} {distance} {tile_or_tiles}") } else { format!("{} {}", distance, tile_or_tiles) } } else { format!("{} {}", distance, tile_or_tiles) } } } #[derive(Component, Copy, Clone, Debug, Deref, DerefMut, PartialEq)] pub struct Area(pub AABB); #[derive(Component, Clone, Copy, Debug, Default, Reflect)] #[reflect(Component)] pub struct Player; #[derive(Clone, Copy, Debug)] pub struct Pool { pub value: T, pub max: T, } impl Default for Pool where T: Default, { fn default() -> Self { Self { value: Default::default(), max: Default::default(), } } } #[derive(Clone, Debug)] pub struct RandomTable(Vec) where T: Clone; impl RandomTable where T: Clone, { pub fn add(&mut self, value: T, weight: i32) -> &mut Self { if weight > 0 { for _ in 0..weight { self.0.push(value.clone()); } } self } } impl Default for RandomTable where T: Clone, { fn default() -> Self { Self(vec![]) } } impl Iterator for RandomTable where T: Clone, { type Item = T; fn next(&mut self) -> Option { let mut rng = thread_rng(); self.0.shuffle(&mut rng); self.0.get(0).cloned() } } fn setup(core_config: Res) { let mut scale = PHYSICS_SCALE.write().unwrap(); *scale = core_config.pixels_per_unit as f32; let mut mode = RELATIVE_DIRECTION_MODE.write().unwrap(); *mode = core_config.relative_direction_mode; } #[derive(Clone, Copy, Debug)] pub struct CoreConfig { pub relative_direction_mode: RelativeDirectionMode, pub pixels_per_unit: u8, } impl Default for CoreConfig { fn default() -> Self { Self { relative_direction_mode: RelativeDirectionMode::Directional, pixels_per_unit: 1, } } } fn sync_config(config: Res) { if config.is_changed() { let mut mode = RELATIVE_DIRECTION_MODE.write().unwrap(); *mode = config.relative_direction_mode; } } pub struct CorePlugin(PhantomData); impl Default for CorePlugin { fn default() -> Self { Self(PhantomData) } } impl Plugin for CorePlugin { fn build(&self, app: &mut App) { if !app.world.contains_resource::() { app.insert_resource(CoreConfig::default()); } let config = *app.world.get_resource::().unwrap(); app.add_plugin(RapierPhysicsPlugin::::pixels_per_meter( config.pixels_per_unit as f32, )) .add_startup_system(setup) .add_system(sync_config); } } pub struct CorePlugins(PhantomData); impl Default for CorePlugins { fn default() -> Self { Self(PhantomData) } } impl PluginGroup for CorePlugins { fn build(&mut self, group: &mut bevy::app::PluginGroupBuilder) { group .add(crate::bevy_tts::TtsPlugin) .add(crate::bevy_synthizer::SynthizerPlugin) .add(CorePlugin::::default()); } }