use std::{error::Error, fmt::Debug, hash::Hash, marker::PhantomData}; use bevy::prelude::*; use bevy_rapier2d::prelude::*; use bevy_tts::Tts; use leafwing_input_manager::prelude::*; use crate::{ core::{Player, PointLike}, error::error_handler, map::Map, pathfinding::Destination, visibility::{RevealedTiles, Viewshed, Visible, VisibleEntities}, }; #[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug)] pub enum ExplorationAction { Forward, Backward, Left, Right, FocusNext, FocusPrev, SelectNextType, SelectPrevType, NavigateTo, } #[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; #[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) where T: Component + Default; #[derive(Component, Clone, Copy, Debug, Default, Reflect)] #[reflect(Component)] pub struct Mappable; fn exploration_type_change( mut tts: ResMut, mut explorers: Query<( &ActionState, &VisibleEntities, &mut FocusedExplorationType, )>, features: Query<&ExplorationType>, ) -> Result<(), Box> where ExplorationType: Component + Default + Copy + Ord, State: 'static + Clone + Debug + Eq + Hash + Send + Sync, { for (actions, 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 actions.just_pressed(ExplorationAction::SelectPrevType) { 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 actions.just_pressed(ExplorationAction::SelectNextType) { 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, mut tts: ResMut, explorers: Query<( Entity, &ActionState, &VisibleEntities, &FocusedExplorationType, Option<&Exploring>, )>, features: Query<(Entity, &Transform, &ExplorationType)>, ) -> Result<(), Box> where ExplorationType: Component + Default + PartialEq, State: 'static + Clone + Debug + Eq + Hash + Send + Sync, { for (entity, actions, visible_entities, focused_type, exploring) in explorers.iter() { let mut features = features .iter() .filter(|v| visible_entities.contains(&v.0)) .map(|v| (v.1.trunc(), 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 actions.just_pressed(ExplorationAction::FocusNext) { 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 actions.just_pressed(ExplorationAction::FocusPrev) { 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.trunc())); } } Ok(()) } fn exploration_type_changed_announcement( mut tts: ResMut, focused: Query< ( &FocusedExplorationType, Ref>, ), Changed>, >, ) -> Result<(), Box> where ExplorationType: Component + Default + Copy + Into, { for (focused, changed) in &focused { 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, map: Query<&Map>, explorers: Query< ( Entity, &ActionState, &Transform, Option<&Exploring>, ), With, >, ) where State: 'static + Clone + Debug + Eq + Hash + Send + Sync, MapData: 'static + Clone + Default + Send + Sync, { for (entity, actions, transform, exploring) in explorers.iter() { let coordinates = transform.translation; let mut exploring = if let Some(exploring) = exploring { **exploring } else { let floor = coordinates.floor(); (floor.x, floor.y) }; let orig = exploring; if actions.just_pressed(ExplorationAction::Forward) { exploring.1 += 1.; } else if actions.just_pressed(ExplorationAction::Backward) { exploring.1 -= 1.; } else if actions.just_pressed(ExplorationAction::Left) { exploring.0 -= 1.; } else if actions.just_pressed(ExplorationAction::Right) { exploring.0 += 1.; } let dimensions = if let Ok(map) = map.get_single() { Some((map.width as f32, map.height as f32)) } else { None }; if let Some((width, height)) = dimensions { if exploring.0 >= width || exploring.1 >= height { return; } } if orig != exploring && exploring.0 >= 0. && exploring.1 >= 0. { commands.entity(entity).insert(Exploring(exploring)); } } } fn navigate_to_explored( mut commands: Commands, map: Query<(&Map, &RevealedTiles)>, explorers: Query<(Entity, &ActionState, &Exploring)>, ) where State: 'static + Clone + Debug + Eq + Hash + Send + Sync, MapData: 'static + Clone + Default + Send + Sync, { for (entity, actions, 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 actions.just_pressed(ExplorationAction::NavigateTo) && 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<(&Transform, &Exploring, &Viewshed), Changed>, focused: Query>, explorable: Query, With)>>, names: Query<&Name>, types: Query<&ExplorationType>, mappables: Query<&Mappable>, rapier_context: Res, ) -> Result<(), Box> where ExplorationType: Component + Copy + Into, MapData: 'static + Clone + Default + Send + Sync, { if let Ok((coordinates, exploring, viewshed)) = explorer.get_single() { let coordinates = coordinates.trunc(); let point = **exploring; let shape = Collider::cuboid(0.5 - f32::EPSILON, 0.5 - f32::EPSILON); let (known, idx) = if let Ok((map, revealed_tiles)) = map.get_single() { let idx = point.to_index(map.width); (revealed_tiles[idx], Some(idx)) } else { (false, None) }; let visible = viewshed.is_point_visible(exploring); let fog_of_war = !visible && known; let description: String = if known || visible { let mut tokens: Vec = vec![]; for entity in focused.iter() { commands.entity(entity).remove::(); } let exploring = Vec2::new(exploring.x(), exploring.y()); rapier_context.intersections_with_shape( exploring, 0., &shape, QueryFilter::new().predicate(&|v| explorable.get(v).is_ok()), |entity| { commands.entity(entity).insert(ExplorationFocused); if visible || mappables.get(entity).is_ok() { if let Ok(name) = names.get(entity) { tokens.push(name.to_string()); } if tokens.is_empty() { if let Ok(t) = types.get(entity) { tokens.push((*t).into()); } } } true }, ); if tokens.is_empty() { if let Some(idx) = idx { if let Ok((map, _)) = map.get_single() { let tile = map.tiles[idx]; if tile.is_blocked() { tokens.push("wall".to_string()); } else { tokens.push("floor".to_string()); } } } tokens.first().cloned().unwrap_or_default() } else { tokens.join(": ") } } else { "Unknown".to_string() }; let mut tokens = vec![ description, coordinates.direction_and_distance(exploring, None), ]; if fog_of_war { tokens.push("in the fog of war".to_string()); } tts.speak(tokens.join(", "), 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(Resource, Clone, Debug, Default)] struct ExplorationConfig { states: Vec, } #[derive(Resource, Clone, Default)] pub struct ExplorationPlugin { pub states: Vec, pub exploration_type: PhantomData, pub map_data: PhantomData, } impl Plugin for ExplorationPlugin where ExplorationType: 'static + Component + Default + Copy + Ord + PartialEq + Into, State: States, MapData: 'static + Clone + Default + Send + Sync, { fn build(&self, app: &mut App) { let config = ExplorationConfig { states: self.states.clone(), }; app.insert_resource(config.clone()) .register_type::() .register_type::() .register_type::() .add_plugin(InputManagerPlugin::::default()) .add_system( exploration_changed_announcement::.pipe(error_handler), ); if config.states.is_empty() { app.add_systems(( exploration_focus::, exploration_type_focus::.pipe(error_handler), exploration_type_change::.pipe(error_handler), navigate_to_explored::, )) .add_system( exploration_type_changed_announcement:: .pipe(error_handler) .in_base_set(CoreSet::PostUpdate), ); } else { let states = config.states; for state in states { app.add_systems( ( exploration_focus::, exploration_type_focus::.pipe(error_handler), exploration_type_change::.pipe(error_handler), navigate_to_explored::, exploration_type_changed_announcement:: .pipe(error_handler), ) .in_set(OnUpdate(state.clone())), ) .add_system(cleanup.in_schedule(OnExit(state))); } } } }