use std::{ cmp::{max, min}, f32::consts::PI, fmt::Display, sync::RwLock, }; use avian2d::{ parry::{ math::Isometry, query::{closest_points, distance, ClosestPoints}, }, prelude::*, }; use bevy::{ app::PluginGroupBuilder, math::{CompassOctant, CompassQuadrant, FloatOrd}, prelude::*, }; use once_cell::sync::Lazy; use rand::prelude::*; use serde::{Deserialize, Serialize}; fn relative_desc(rot: &Rot2) -> String { let mode = RELATIVE_DIRECTION_MODE.read().unwrap(); match rot.as_radians() { v if v <= PI / 12. && v > -PI / 12. => "ahead", v if v <= PI / 4. && v > PI / 12. => { if *mode == RelativeDirectionMode::ClockFacing { "11:00" } else { "ahead and left" } } v if v <= 3. * PI / 8. && v > PI / 4. => { if *mode == RelativeDirectionMode::ClockFacing { "10:00" } else { "left and ahead" } } v if v <= 5. * PI / 8. && v > 3. * PI / 8. => "left", v if v <= 3. * PI / 4. && v > 5. * PI / 8. => { if *mode == RelativeDirectionMode::ClockFacing { "8:00" } else { "left and behind" } } v if v <= 11. * PI / 12. && v > 3. * PI / 4. => { if *mode == RelativeDirectionMode::ClockFacing { "7:00" } else { "behind and left" } } v if v <= PI && v > 11. * PI / 12. || v > -PI && v <= -11. * PI / 12. => "behind", v if v <= -3. * PI / 4. && v > -11. * PI / 12. => { if *mode == RelativeDirectionMode::ClockFacing { "5:00" } else { "behind and right" } } v if v <= -5. * PI / 8. && v > -3. * PI / 4. => { if *mode == RelativeDirectionMode::ClockFacing { "4:00" } else { "right and behind" } } v if v <= -3. * PI / 8. && v > -5. * PI / 8. => "right", v if v <= -PI / 4. && v > -3. * PI / 8. => { if *mode == RelativeDirectionMode::ClockFacing { "2:00" } else { "right and ahead" } } v if v <= -PI / 12. && v > -PI / 4. => { if *mode == RelativeDirectionMode::ClockFacing { "1:00" } else { "ahead and right" } } _ => "ahead", } .into() } #[derive(Clone, Copy, Debug, Eq, PartialEq, Deref, DerefMut, Reflect)] pub struct MovementDirection(CompassOctant); impl From for MovementDirection { fn from(rot: Rot2) -> Self { use CompassOctant::*; MovementDirection(match rot.as_radians() { h if h > -PI / 8. && h <= PI / 8. => East, h if h > PI / 8. && h <= 3. * PI / 8. => NorthEast, h if h > 3. * PI / 8. && h <= 5. * PI / 8. => North, h if h > 5. * PI / 8. && h <= 7. * PI / 8. => NorthWest, h if h > 7. * PI / 8. || h <= -7. * PI / 8. => West, h if h > -7. * PI / 8. && h <= -5. * PI / 8. => SouthWest, h if h > -5. * PI / 8. && h <= -3. * PI / 8. => South, h if h > -3. * PI / 8. && h <= -PI / 8. => SouthEast, _ => West, }) } } // Converting from strings into directions doesn't make sense. #[allow(clippy::from_over_into)] impl Into for MovementDirection { fn into(self) -> String { use CompassOctant::*; match self.0 { North => "north", NorthEast => "northeast", East => "east", SouthEast => "southeast", South => "south", SouthWest => "southwest", West => "west", NorthWest => "northwest", } .into() } } 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, Deref, DerefMut, Reflect)] #[reflect(Component)] pub struct CardinalDirection(pub CompassQuadrant); impl Default for CardinalDirection { fn default() -> Self { Self(CompassQuadrant::East) } } impl From for CardinalDirection { fn from(rot: Rot2) -> Self { use CompassQuadrant::*; CardinalDirection(match rot.as_radians() { h if h > -PI / 4. && h <= PI / 4. => East, h if h > PI / 4. && h <= 3. * PI / 4. => North, h if h > 3. * PI / 4. || h <= -3. * PI / 4. => West, _ => South, }) } } impl From<&CardinalDirection> for Rot2 { fn from(direction: &CardinalDirection) -> Self { use CompassQuadrant::*; match direction.0 { North => Rot2::radians(PI / 2.), East => Rot2::radians(0.), South => Rot2::radians(-PI / 2.), West => Rot2::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 CompassQuadrant::*; match self.0 { 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 trunc(&self) -> (f32, f32) { (self.x().trunc(), self.y().trunc()) } fn to_index(&self, width: usize) -> usize { (self.y_usize() * width) + self.x_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) -> Rot2 { let y = other.y() - self.y(); let x = other.x() - self.x(); Rot2::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 yaw = yaw.as_radians(); let bearing = self.bearing(other).as_radians(); let rot = Rot2::radians(bearing - yaw); relative_desc(&rot) } 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 IVec2 { fn x(&self) -> f32 { self.x as f32 } fn y(&self) -> f32 { self.y as f32 } } 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 } } pub trait TransformExt { fn yaw(&self) -> Rot2; } impl TransformExt for Transform { fn yaw(&self) -> Rot2 { let forward = self.right(); Rot2::radians(forward.y.atan2(forward.x)) } } pub trait GlobalTransformExt { fn yaw(&self) -> Rot2; 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) -> Rot2 { let forward = self.right(); Rot2::radians(forward.y.atan2(forward.x)) } fn closest_points( &self, collider: &Collider, other: &GlobalTransform, other_collider: &Collider, ) -> ClosestPoints { let scale = PHYSICS_SCALE.read().unwrap(); let pos1 = Isometry::new( (self.translation() / *scale).xy().into(), self.yaw().as_radians(), ); let pos2 = Isometry::new( (other.translation() / *scale).xy().into(), other.yaw().as_radians(), ); closest_points( &pos1, collider.shape().as_ref(), &pos2, other_collider.shape().as_ref(), f32::MAX, ) .unwrap() } fn collider_direction_and_distance( &self, collider: &Collider, other: &GlobalTransform, other_collider: &Collider, ) -> String { let scale = PHYSICS_SCALE.read().unwrap(); let pos1 = Isometry::new( (self.translation() / *scale).xy().into(), self.yaw().as_radians(), ); let pos2 = Isometry::new( (other.translation() / *scale).xy().into(), other.yaw().as_radians(), ); let closest = self.closest_points(collider, other, other_collider); let distance = distance( &pos1, collider.shape().as_ref(), &pos2, other_collider.shape().as_ref(), ) .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).as_radians(); let yaw = self.yaw().as_radians(); let rot = Rot2::radians(bearing - yaw); let direction = relative_desc(&rot); format!("{direction} {distance} {tile_or_tiles}") } else { format!("{} {}", distance, tile_or_tiles) } } else { format!("{} {}", distance, tile_or_tiles) } } } #[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.first().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(Resource, Clone, Copy, Debug)] pub struct CoreConfig { pub relative_direction_mode: RelativeDirectionMode, pub pixels_per_unit: u8, } 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 { pub relative_direction_mode: RelativeDirectionMode, pub pixels_per_unit: u8, } impl Default for CorePlugin { fn default() -> Self { Self { relative_direction_mode: RelativeDirectionMode::Directional, pixels_per_unit: 1, } } } impl Plugin for CorePlugin { fn build(&self, app: &mut App) { let config = CoreConfig { relative_direction_mode: self.relative_direction_mode, pixels_per_unit: self.pixels_per_unit, }; app.insert_resource(config) .register_type::() .add_plugins(PhysicsPlugins::default().with_length_unit(config.pixels_per_unit as f32)) .add_systems(Startup, setup) .add_systems(Update, sync_config); } } pub struct CorePlugins; impl PluginGroup for CorePlugins { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() .add(crate::bevy_tts::TtsPlugin) .add(crate::bevy_synthizer::SynthizerPlugin::default()) .add(crate::navigation::NavigationPlugin::default()) .add(CorePlugin::default()) } }