diff --git a/crates/border-wars/assets/selection.png b/crates/border-wars/assets/selection.png new file mode 100644 index 0000000..40e276a Binary files /dev/null and b/crates/border-wars/assets/selection.png differ diff --git a/crates/border-wars/src/main.rs b/crates/border-wars/src/main.rs index b02c626..eae5622 100644 --- a/crates/border-wars/src/main.rs +++ b/crates/border-wars/src/main.rs @@ -4,6 +4,7 @@ use bevy::prelude::*; use border_wars::camera::CameraPlugin; use border_wars::map::renderer::RendererPlugin; use border_wars::scenes::ScenesPlugin; +use border_wars::map::selection::SelectorPlugin; fn main() { App::new() @@ -11,5 +12,6 @@ fn main() { .add_plugins(ScenesPlugin) .add_plugins(RendererPlugin) .add_plugins(CameraPlugin) + .add_plugins(SelectorPlugin) .run(); } diff --git a/crates/border-wars/src/map/mod.rs b/crates/border-wars/src/map/mod.rs index 73d0c06..24c15d6 100644 --- a/crates/border-wars/src/map/mod.rs +++ b/crates/border-wars/src/map/mod.rs @@ -3,6 +3,7 @@ pub mod generation; pub mod hex; pub mod renderer; +pub mod selection; use bevy::prelude::*; diff --git a/crates/border-wars/src/map/renderer.rs b/crates/border-wars/src/map/renderer.rs index ca79ab6..e1363db 100644 --- a/crates/border-wars/src/map/renderer.rs +++ b/crates/border-wars/src/map/renderer.rs @@ -40,7 +40,7 @@ impl Tile { /// /// TODO: we are currently using temporary images that will modify /// this function in the future. - const fn get_image_size(&self) -> Vec2 { + pub const fn get_image_size(&self) -> Vec2 { match self { Self::Grass => Vec2 { x: 1250.0, diff --git a/crates/border-wars/src/map/selection.rs b/crates/border-wars/src/map/selection.rs new file mode 100644 index 0000000..8967a3f --- /dev/null +++ b/crates/border-wars/src/map/selection.rs @@ -0,0 +1,138 @@ +//! All programs related to the selection of tiles. + +use bevy::prelude::*; +use bevy::sprite::Anchor; + +use super::Tile; + +/// A component that represents a selected tile. +#[derive(Component)] +pub struct SelectedTile; + +/// A component that represents the selection. +#[derive(Component)] +struct Selection; + +/// A event that is triggered when a mouse button is clicked in the world. +/// +/// The event contains the position of the cursor in the world. +#[derive(Event)] +struct ClickOnTheWorld(Vec2); + +/// A plugin that handles and render the selection of tiles. +pub struct SelectorPlugin; + +impl Plugin for SelectorPlugin { + fn build(&self, app: &mut App) { + app.add_systems(PreUpdate, mouse_handler) + .add_systems(PreUpdate, select_closest_tile) + .add_systems(Update, render_selection) + .add_event::(); + } +} + +/// Handles the mouse click and gets the position of the cursor in the world.* +/// Finally, it sends an event with the position of the cursor. +fn mouse_handler( + mouse_button_input: Res>, + windows: Query<&Window>, + cameras: Query<(&Camera, &GlobalTransform)>, + mut events_writer: EventWriter, +) { + if !mouse_button_input.just_pressed(MouseButton::Left) { + return; + } + + let cursor_position_on_screen = windows + .get_single() + .expect("Main window not found") + .cursor_position(); + + let Some(cursor_position_on_screen) = cursor_position_on_screen else { + return; + }; + + let (camera, camera_transform) = cameras.get_single().expect("Camera not found"); + + let cursor_position_in_world = camera + .viewport_to_world(camera_transform, cursor_position_on_screen) + .expect("Failed to convert cursor position") + .origin + .truncate(); + + events_writer.send(ClickOnTheWorld(cursor_position_in_world)); +} + +/// Selects the closest tile to the cursor and marks it as selected. +/// It also marks the old selection as unselected. +fn select_closest_tile( + mut commands: Commands, + tiles: Query<(Entity, &Transform, &Tile)>, + mut old_selection: Query>, + mut selected_tile: Local>, + mut click_event_reader: EventReader, +) { + for click_event in click_event_reader.read() { + // The closest tile and its distance to the cursor. + let mut closest_entity: Option = None; + let mut closest_position: Option = None; + + for (tile_entity, tile_transform, tile_type) in tiles.iter() { + let mut tile_position = tile_transform.translation.truncate(); + let tile_size = tile_type.get_image_size(); + let tile_scale = tile_transform.scale.truncate(); + + tile_position.x -= (tile_size.x / 2.0) * tile_scale.x; + tile_position.y += (tile_size.y / 2.0) * tile_scale.y; + + let distance_to_cursor = tile_position.distance(click_event.0); + + if closest_position.is_none() || closest_position > Some(distance_to_cursor) { + closest_entity = Some(tile_entity); + closest_position = Some(distance_to_cursor); + } + } + if let Some(tile_entity) = closest_entity { + if *selected_tile == Some(tile_entity.index()) { + return; + } + commands.entity(tile_entity).insert(SelectedTile); + *selected_tile = Some(tile_entity.index()); + } + + for entity in old_selection.iter_mut() { + commands.entity(entity).remove::(); + } + } +} + +/// Renders the selection with a sprite. +/// +/// The position updates when the selected tile changes. +fn render_selection( + mut commands: Commands, + asset_server: Res, + query: Query<&Transform, Added>, + mut selection_query: Query<&mut Transform, (With, Without)>, +) { + for target_transform in query.iter() { + let mut transform = *target_transform; + transform.translation.z += 1.0; + if selection_query.is_empty() { + commands + .spawn(SpriteBundle { + sprite: Sprite { + anchor: Anchor::BottomRight, + ..Default::default() + }, + transform, + // TODO: This image is a temporary image of the selection. + texture: asset_server.load("selection.png"), + ..default() + }) + .insert(Selection); + } else if let Ok(mut selection_transform) = selection_query.get_single_mut() { + *selection_transform = transform; + } + } +}