use std::{error::Error, fmt::Debug, hash::Hash, marker::PhantomData}; use bevy::prelude::*; use bevy_input_actionmap::InputMap; use bevy_rapier2d::prelude::*; use bevy_tts::Tts; use derive_more::{Deref, DerefMut}; use crate::{ core::{Coordinates, Player, PointLike}, error::error_handler, map::Map, pathfinding::Destination, visibility::{RevealedTiles, Viewshed, Visible, VisibleEntities}, }; #[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)] #[reflect(Component)] pub struct Explorable; #[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)] #[reflect(Component)] pub struct ExplorationFocused; #[allow(dead_code)] #[derive(Component, Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Reflect)] pub enum ExplorationType { Portal = 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::Portal => "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::Portal => "exit", ExplorationType::Item => "item", ExplorationType::Character => "character", ExplorationType::Ally => "ally", ExplorationType::Enemy => "enemy", } } } #[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)] #[reflect(Component)] pub struct Exploring(pub (f32, f32)); impl_pointlike_for_tuple_component!(Exploring); #[derive(Component, Clone, Debug, Default, Deref, DerefMut)] pub struct FocusedExplorationType(pub Option); #[derive(Component, Clone, Copy, Debug, Default, Reflect)] #[reflect(Component)] pub struct Mappable; fn exploration_type_change( config: Res>, mut tts: ResMut, input: Res>, mut explorers: Query<(&VisibleEntities, &mut FocusedExplorationType)>, features: Query<&ExplorationType>, ) -> Result<(), Box> where S: 'static + Clone + Debug + Eq + Hash + Send + Sync, A: Hash + Eq + Clone + Send + Sync, { if let (Some(select_next_type), Some(select_prev_type)) = ( config.action_explore_select_next_type.clone(), config.action_explore_select_prev_type.clone(), ) { let changed = input.just_active(select_next_type.clone()) || input.just_active(select_prev_type.clone()); if !changed { return Ok(()); } for (visible, mut focused) in explorers.iter_mut() { let mut types: Vec = vec![]; for e in visible.iter() { if let Ok(t) = features.get(*e) { types.push(*t); } } types.sort(); types.dedup(); if types.is_empty() { tts.speak("Nothing visible.", true)?; } else if input.just_active(select_prev_type.clone()) { 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(select_next_type.clone()) { 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, config: Res>, input: Res>, mut tts: ResMut, explorers: Query<( Entity, &VisibleEntities, &FocusedExplorationType, Option<&Exploring>, )>, features: Query<(Entity, &Coordinates, &ExplorationType)>, ) -> Result<(), Box> where S: 'static + Clone + Debug + Eq + Hash + Send + Sync, A: Hash + Eq + Clone + Send + Sync, { if let (Some(explore_focus_next), Some(explore_focus_prev)) = ( config.action_explore_focus_next.clone(), config.action_explore_focus_prev.clone(), ) { let changed = input.just_active(explore_focus_next.clone()) || input.just_active(explore_focus_prev.clone()); if !changed { return Ok(()); } for (entity, visible_entities, focused_type, exploring) in explorers.iter() { let mut features = features .iter() .filter(|v| visible_entities.contains(&v.0)) .map(|v| (v.1.floor(), v.2)) .collect::>(); if features.is_empty() { tts.speak("Nothing visible.", true)?; return Ok(()); } features.sort_by(|(c1, _), (c2, _)| c1.partial_cmp(c2).unwrap()); if let Some(focused) = &focused_type.0 { features.retain(|(_, t)| **t == *focused); } let mut target: Option<&((f32, f32), &ExplorationType)> = None; if input.just_active(explore_focus_next.clone()) { 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(explore_focus_prev.clone()) { 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.floor())); } } } 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, config: Res>, input: Res>, map: Query<&Map>, explorers: Query<(Entity, &Coordinates, Option<&Exploring>), With>, ) where S: 'static + Clone + Debug + Eq + Hash + Send + Sync, A: Hash + Eq + Clone + Send + Sync, { if let ( Some(explore_forward), Some(explore_backward), Some(explore_left), Some(explore_right), ) = ( config.action_explore_forward.clone(), config.action_explore_backward.clone(), config.action_explore_left.clone(), config.action_explore_right.clone(), ) { for map in map.iter() { if let Ok((entity, coordinates, exploring)) = explorers.get_single() { let coordinates = **coordinates; let mut exploring = if let Some(exploring) = exploring { **exploring } else { coordinates.floor() }; let orig = exploring; if input.just_active(explore_forward.clone()) { exploring.1 += 1.; } else if input.just_active(explore_backward.clone()) { exploring.1 -= 1.; } else if input.just_active(explore_left.clone()) { exploring.0 -= 1.; } else if input.just_active(explore_right.clone()) { 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, config: Res>, input: Res>, map: Query<(&Map, &RevealedTiles)>, explorers: Query<(Entity, &Exploring)>, ) where S: 'static + Clone + Debug + Eq + Hash + Send + Sync, A: Hash + Eq + Clone + Send + Sync, { if let Some(navigate_to_explored) = config.action_navigate_to_explored.clone() { 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(navigate_to_explored.clone()) && 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)>, explorer: Query<(&Coordinates, &Exploring, &Viewshed), Changed>, focused: Query>, explorable: Query, With)>>, names: Query<&Name>, types: Query<&ExplorationType>, mappables: Query<&Mappable>, query_pipeline: Res, collider_query: QueryPipelineColliderComponentsQuery, ) -> Result<(), Box> { if let Ok((coordinates, exploring, viewshed)) = explorer.get_single() { let collider_set = QueryPipelineColliderComponentsSet(&collider_query); let coordinates = coordinates.floor(); for (map, revealed_tiles) in map.iter() { let point = **exploring; let idx = point.to_index(map.width); let shape = Cuboid::new(Vec2::new(0.49, 0.49).into()); let known = revealed_tiles[idx]; let visible = viewshed.is_point_visible(exploring); 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::(); } let shape_pos = (Vec2::new(exploring.x(), exploring.y()), 0.); query_pipeline.intersections_with_shape( &collider_set, &shape_pos.into(), &shape, InteractionGroups::all(), Some(&|v| explorable.get(v.entity()).is_ok()), |handle| { let entity = handle.entity(); commands.entity(entity).insert(ExplorationFocused); 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()); } } } true }, ); if tokens.is_empty() { let tile = map.tiles[idx]; if tile.is_blocked() { "wall".to_string() } else { "floor".to_string() } } else { tokens.join(": ") } } else { "Unknown".to_string() }; let mut tokens: Vec = vec![ description, coordinates.direction_and_distance(exploring, None), ]; if fog_of_war { tokens.push("in the fog of war".into()); } tts.speak(tokens.join(", ").to_string(), true)?; } } Ok(()) } fn cleanup( mut commands: Commands, explorers: Query>, focus: Query>, ) { for entity in explorers.iter() { commands.entity(entity).remove::(); } for entity in focus.iter() { commands.entity(entity).remove::(); } } #[derive(Clone, Debug)] pub struct ExplorationConfig { pub exploration_control_states: Vec, pub action_explore_forward: Option, pub action_explore_backward: Option, pub action_explore_left: Option, pub action_explore_right: Option, pub action_explore_focus_next: Option, pub action_explore_focus_prev: Option, pub action_explore_select_next_type: Option, pub action_explore_select_prev_type: Option, pub action_navigate_to_explored: Option, } impl Default for ExplorationConfig { fn default() -> Self { Self { exploration_control_states: vec![], action_explore_forward: None, action_explore_backward: None, action_explore_left: None, action_explore_right: None, action_explore_focus_next: None, action_explore_focus_prev: None, action_explore_select_next_type: None, action_explore_select_prev_type: None, action_navigate_to_explored: None, } } } pub struct ExplorationPlugin<'a, S, A, D>(PhantomData<&'a S>, PhantomData<&'a A>, PhantomData); impl<'a, S, A, D> Default for ExplorationPlugin<'a, S, A, D> { fn default() -> Self { Self(PhantomData, PhantomData, PhantomData) } } impl<'a, S, A, D> Plugin for ExplorationPlugin<'a, S, A, D> where S: Clone + Debug + Eq + Hash + Send + Sync, A: Hash + Eq + Clone + Send + Sync, D: 'static + Clone + Default + Send + Sync, 'a: 'static, { fn build(&self, app: &mut App) { if !app.world.contains_resource::>() { app.insert_resource(ExplorationConfig::::default()); } let config = app .world .get_resource::>() .unwrap() .clone(); app.register_type::() .register_type::() .register_type::() .add_system_to_stage( CoreStage::PostUpdate, exploration_changed_announcement::.chain(error_handler), ); if config.exploration_control_states.is_empty() { app.add_system(exploration_focus::) .add_system(exploration_type_focus::.chain(error_handler)) .add_system(exploration_type_change::.chain(error_handler)) .add_system(navigate_to_explored::) .add_system_to_stage( CoreStage::PostUpdate, exploration_type_changed_announcement.chain(error_handler), ); } else { let states = config.exploration_control_states; for state in states { app.add_system_set( SystemSet::on_update(state.clone()) .with_system(exploration_focus::) .with_system(exploration_type_focus::.chain(error_handler)) .with_system(exploration_type_change::.chain(error_handler)) .with_system(navigate_to_explored::) .with_system(exploration_type_changed_announcement.chain(error_handler)), ) .add_system_set(SystemSet::on_exit(state).with_system(cleanup)); } } } }