use std::{ collections::HashMap, error::Error, f32::consts::PI, fmt::Debug, hash::Hash, marker::PhantomData, }; use bevy::prelude::*; use bevy_rapier2d::prelude::*; use bevy_tts::Tts; use leafwing_input_manager::prelude::*; use crate::{ commands::RunIfExistsExt, core::{Angle, Area, CardinalDirection, GlobalTransformExt, Player}, error::error_handler, exploration::{ExplorationFocused, Exploring}, log::Log, pathfinding::Destination, utils::target_and_other, }; #[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug)] pub enum NavigationAction { Move, Rotate, SnapLeft, SnapRight, SnapCardinal, SnapReverse, Sprint, } #[derive(Component, Clone, Copy, Debug, Deref, DerefMut, Reflect)] #[reflect(Component)] pub struct MaxSpeed(pub f32); impl Default for MaxSpeed { fn default() -> Self { MaxSpeed(2.) } } #[derive(Component, 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(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)] #[reflect(Component)] pub struct Speed(pub f32); #[derive(Component, Deref, DerefMut)] struct SnapTimer(Timer); impl Default for SnapTimer { fn default() -> Self { Self(Timer::from_seconds(0.2, false)) } } fn movement_controls<S>( mut commands: Commands, config: Res<NavigationConfig<S>>, time: Res<Time>, snap_timers: Res<HashMap<Entity, SnapTimer>>, mut query: Query<( Entity, &ActionState<NavigationAction>, &mut Velocity, &mut Speed, &MaxSpeed, Option<&RotationSpeed>, &mut Transform, Option<&Destination>, )>, exploration_focused: Query<Entity, With<ExplorationFocused>>, ) where S: 'static + Clone + Debug + Eq + Hash + Send + Sync, { for ( entity, actions, mut velocity, mut speed, max_speed, rotation_speed, mut transform, destination, ) in &mut query { let sprinting = actions.pressed(NavigationAction::Sprint); let mut cleanup = false; if actions.pressed(NavigationAction::Move) { if let Some(pair) = actions.clamped_axis_pair(NavigationAction::Move) { cleanup = true; let direction = pair.xy(); let forward_backward_movement_factor = if direction.x > 0. { config.forward_movement_factor } else if direction.x < 0. { config.backward_movement_factor } else { 0. }; let movement_factor = if direction.x != 0. && direction.y != 0. { config .strafe_movement_factor .min(forward_backward_movement_factor) } else if direction.y != 0. { config.strafe_movement_factor } else { forward_backward_movement_factor }; let mut s = if sprinting { **max_speed } else { **max_speed / config.sprint_movement_factor }; s *= movement_factor; **speed = s; let mut v = direction * **speed; v = transform .compute_matrix() .transform_vector3(v.extend(0.)) .truncate(); velocity.linvel = v; } } else if destination.is_none() { velocity.linvel = Vec2::ZERO; } if !snap_timers.contains_key(&entity) { if let Some(rotation_speed) = rotation_speed { if actions.pressed(NavigationAction::Rotate) { cleanup = true; let delta = rotation_speed.radians() * time.delta_seconds() * actions.clamped_value(NavigationAction::Rotate); transform.rotate(Quat::from_rotation_z(delta)); } } else { velocity.angvel = 0.; } } else { velocity.angvel = 0.; } if cleanup { commands.entity(entity).remove::<Destination>(); commands.entity(entity).remove::<Exploring>(); for entity in exploration_focused.iter() { commands.entity(entity).remove::<ExplorationFocused>(); } } } } fn snap( mut tts: ResMut<Tts>, mut snap_timers: ResMut<HashMap<Entity, SnapTimer>>, mut query: Query< ( Entity, &ActionState<NavigationAction>, &mut Transform, &mut Velocity, &CardinalDirection, ), With<Player>, >, ) -> Result<(), Box<dyn Error>> { for (entity, actions, mut transform, mut velocity, direction) in query.iter_mut() { if snap_timers.contains_key(&entity) { continue; } else if actions.pressed(NavigationAction::SnapLeft) { snap_timers.insert(entity, SnapTimer::default()); transform.rotation = Quat::from_rotation_z(match direction { CardinalDirection::North => PI, CardinalDirection::East => PI / 2., CardinalDirection::South => 0., CardinalDirection::West => -PI / 2., }); velocity.angvel = 0.; } else if actions.pressed(NavigationAction::SnapRight) { snap_timers.insert(entity, SnapTimer::default()); transform.rotation = Quat::from_rotation_z(match direction { CardinalDirection::North => 0., CardinalDirection::East => -PI / 2., CardinalDirection::South => PI, CardinalDirection::West => PI / 2., }); velocity.angvel = 0.; } else if actions.pressed(NavigationAction::SnapReverse) { snap_timers.insert(entity, SnapTimer::default()); transform.rotate(Quat::from_rotation_z(PI)); velocity.angvel = 0.; } else if actions.pressed(NavigationAction::SnapCardinal) { let yaw: Angle = direction.into(); let yaw = yaw.radians(); transform.rotation = Quat::from_rotation_z(yaw); tts.speak(direction.to_string(), true)?; velocity.angvel = 0.; } } Ok(()) } fn tick_snap_timers(time: Res<Time>, mut snap_timers: ResMut<HashMap<Entity, SnapTimer>>) { for timer in snap_timers.values_mut() { timer.tick(time.delta()); } snap_timers.retain(|_, v| !v.finished()); } fn update_direction( mut commands: Commands, mut query: Query< (Entity, &GlobalTransform, Option<&mut CardinalDirection>), (With<Player>, Changed<Transform>), >, ) { for (entity, transform, direction) in query.iter_mut() { let yaw = transform.yaw(); let new_direction: CardinalDirection = yaw.into(); if let Some(mut direction) = direction { if *direction != new_direction { *direction = new_direction; } } else { commands.entity(entity).insert(new_direction); } } } fn remove_direction( mut commands: Commands, removed: RemovedComponents<Transform>, directions: Query<&CardinalDirection>, ) { for entity in removed.iter() { if directions.contains(entity) { commands.run_if_exists(entity, |mut entity| { entity.remove::<CardinalDirection>(); }); } } } fn speak_direction( mut tts: ResMut<Tts>, player: Query< (&CardinalDirection, ChangeTrackers<CardinalDirection>), (With<Player>, Changed<CardinalDirection>), >, ) -> Result<(), Box<dyn Error>> { if let Ok((direction, change)) = player.get_single() { if !change.is_added() { let direction: String = (*direction).into(); tts.speak(direction, true)?; } } Ok(()) } fn add_speed(mut commands: Commands, query: Query<Entity, (Added<Speed>, Without<Velocity>)>) { for entity in query.iter() { commands.entity(entity).insert(Velocity { linvel: Vec2::ZERO, ..default() }); } } pub(crate) fn limit_speed(mut query: Query<(&mut Speed, &MaxSpeed)>) { for (mut speed, max_speed) in &mut query { if **speed > **max_speed { **speed = **max_speed; } } } fn remove_speed(removed: RemovedComponents<Speed>, mut query: Query<&mut Velocity>) { for entity in removed.iter() { if let Ok(mut velocity) = query.get_mut(entity) { velocity.linvel = Vec2::ZERO; } } } fn log_area_descriptions<S, A>( mut events: EventReader<CollisionEvent>, areas: Query<(&Area, Option<&Name>)>, players: Query<&Player>, config: Res<NavigationConfig<S>>, mut log: Query<&mut Log>, ) where S: 'static + Send + Sync, A: 'static + Send + Sync, { if !config.log_area_descriptions { return; } for event in events.iter() { let (entity1, entity2, started) = match event { CollisionEvent::Started(collider1, collider2, _) => (collider1, collider2, true), CollisionEvent::Stopped(collider1, collider2, _) => (collider1, collider2, false), }; if let Some((area, other)) = target_and_other(*entity1, *entity2, &|v| areas.get(v).is_ok()) { if players.get(other).is_ok() { if let Ok((aabb, area_name)) = areas.get(area) { let name = if let Some(name) = area_name { Some(name.to_string()) } else if config.describe_undescribed_areas { Some(format!("{}-by-{} area", aabb.extents().x, aabb.extents().y)) } else { None }; if let Some(name) = name { if let Ok(mut log) = log.get_single_mut() { if started { log.push(format!("Entering {name}.")); } else { log.push(format!("Leaving {name}.")); } } } } } } } } #[derive(Clone, Debug)] pub struct NavigationConfig<S> { pub forward_movement_factor: f32, pub backward_movement_factor: f32, pub strafe_movement_factor: f32, pub sprint_movement_factor: f32, pub movement_control_states: Vec<S>, pub describe_undescribed_areas: bool, pub log_area_descriptions: bool, } impl<S> Default for NavigationConfig<S> { fn default() -> Self { Self { forward_movement_factor: 1., backward_movement_factor: 1., strafe_movement_factor: 1., sprint_movement_factor: 3., movement_control_states: vec![], describe_undescribed_areas: false, log_area_descriptions: true, } } } pub struct NavigationPlugin<'a, S, A>(PhantomData<&'a S>, PhantomData<&'a A>); impl<'a, S, A> Default for NavigationPlugin<'a, S, A> { fn default() -> Self { Self(PhantomData, PhantomData) } } impl<S, A> Plugin for NavigationPlugin<'static, S, A> where S: 'static + Clone + Copy + Debug + Eq + Hash + Send + Sync, A: Hash + Eq + Copy + Send + Sync, { fn build(&self, app: &mut App) { if !app.world.contains_resource::<NavigationConfig<S>>() { app.insert_resource(NavigationConfig::<S>::default()); } let config = app .world .get_resource::<NavigationConfig<S>>() .unwrap() .clone(); app.init_resource::<HashMap<Entity, SnapTimer>>() .register_type::<MaxSpeed>() .register_type::<RotationSpeed>() .add_plugin(InputManagerPlugin::<NavigationAction>::default()) .add_system_to_stage(CoreStage::PreUpdate, update_direction) .add_system_to_stage(CoreStage::PostUpdate, remove_direction) .add_system(tick_snap_timers) .add_system(speak_direction.chain(error_handler)) .add_system(add_speed) .add_system(limit_speed) .add_system_to_stage(CoreStage::PostUpdate, remove_speed) .add_system_to_stage(CoreStage::PostUpdate, log_area_descriptions::<S, A>); const MOVEMENT_CONTROLS: &str = "MOVEMENT_CONTROLS"; if config.movement_control_states.is_empty() { app.add_system( movement_controls::<S> .label(MOVEMENT_CONTROLS) .after(limit_speed), ) .add_system(snap.chain(error_handler).before(MOVEMENT_CONTROLS)); } else { let states = config.movement_control_states; for state in states { app.add_system_set( SystemSet::on_update(state) .with_system(movement_controls::<S>.label(MOVEMENT_CONTROLS)) .with_system(snap.chain(error_handler).before(MOVEMENT_CONTROLS)), ); } } } }