use bevy::{prelude::*, transform::TransformSystems, window::PrimaryWindow}; use crate::{ camera::CameraController, world::{TILE_SIZE, WorldMap}, }; pub struct SelectionPlugin; impl Plugin for SelectionPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() .add_systems(Startup, spawn_tile_info) .add_systems( PostUpdate, (update_hovered, update_selected, draw_highlights, update_tile_info) .chain() .after(TransformSystems::Propagate), ); } } #[derive(Resource, Default, Debug, PartialEq)] pub struct HoveredTile(pub Option); #[derive(Resource, Default, Debug, PartialEq)] pub struct SelectedTile(pub Option); #[derive(Component)] struct TileInfoText; const HOVER_COLOR: Color = Color::srgba(1.0, 1.0, 1.0, 0.6); const SELECT_COLOR: Color = Color::srgb(1.0, 0.85, 0.2); fn update_hovered( map: Res, window: Single<&Window, With>, camera: Single<(&Camera, &GlobalTransform), With>, mut hovered: ResMut, ) { let (camera, global) = *camera; let tile = window .cursor_position() .and_then(|cursor| camera.viewport_to_world_2d(global, cursor).ok()) .and_then(|world| map.tile_at(world)); hovered.set_if_neq(HoveredTile(tile)); } fn update_selected( buttons: Res>, keys: Res>, hovered: Res, mut selected: ResMut, ) { if keys.just_pressed(KeyCode::Escape) { selected.set_if_neq(SelectedTile(None)); } else if buttons.just_pressed(MouseButton::Left) { selected.set_if_neq(SelectedTile(hovered.0)); } } fn draw_highlights(mut gizmos: Gizmos, hovered: Res, selected: Res) { for (tile, color) in [(hovered.0, HOVER_COLOR), (selected.0, SELECT_COLOR)] { if let Some(tile) = tile { gizmos.rect_2d(WorldMap::tile_center(tile), Vec2::splat(TILE_SIZE), color); } } } fn spawn_tile_info(mut commands: Commands) { commands.spawn(( TileInfoText, Text::default(), TextFont::from_font_size(14.0), Node { position_type: PositionType::Absolute, top: px(8), left: px(8), padding: UiRect::all(px(6)), ..default() }, BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.5)), )); } fn update_tile_info( map: Res, hovered: Res, selected: Res, mut text: Single<&mut Text, With>, ) { if !hovered.is_changed() && !selected.is_changed() { return; } text.0 = format!( "Hover: {}\nSelected: {}", describe_tile(&map, hovered.0), describe_tile(&map, selected.0) ); } pub fn describe_tile(map: &WorldMap, tile: Option) -> String { match tile.and_then(|t| map.get(t.as_ivec2()).map(|terrain| (t, terrain))) { Some((t, terrain)) => format!("({}, {}) {terrain:?}", t.x, t.y), None => "-".into(), } } #[cfg(test)] mod tests { use super::*; use crate::world::Terrain; fn selection_app(hovered: Option, selected: Option) -> App { let mut app = App::new(); app.init_resource::>() .init_resource::>() .insert_resource(HoveredTile(hovered)) .insert_resource(SelectedTile(selected)) .add_systems(Update, update_selected); app } fn click(app: &mut App) { app.world_mut() .resource_mut::>() .press(MouseButton::Left); app.update(); } #[test] fn clicking_selects_the_hovered_tile() { let mut app = selection_app(Some(UVec2::new(3, 4)), None); click(&mut app); assert_eq!(app.world().resource::().0, Some(UVec2::new(3, 4))); } #[test] fn clicking_outside_the_world_clears_the_selection() { let mut app = selection_app(None, Some(UVec2::new(3, 4))); click(&mut app); assert_eq!(app.world().resource::().0, None); } #[test] fn escape_clears_the_selection() { let mut app = selection_app(Some(UVec2::ONE), Some(UVec2::new(3, 4))); app.world_mut() .resource_mut::>() .press(KeyCode::Escape); app.update(); assert_eq!(app.world().resource::().0, None); } #[test] fn describe_tile_shows_coordinates_and_terrain() { let map = WorldMap::filled(UVec2::new(4, 4), Terrain::Sand); assert_eq!(describe_tile(&map, Some(UVec2::new(1, 2))), "(1, 2) Sand"); assert_eq!(describe_tile(&map, None), "-"); } }