diff --git a/README.md b/README.md index 73f0c50..f69b5e6 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ cargo run ### Hidden binds (Only available in the debug build) - `Shift + Enter`: Duration of the current phase is set to 3 seconds. -- `Left Mouse Button` on Tile: Rotate tile state. +- `Shift + Left Mouse Button` on Tile: Toggle tile state. - `Shift + Arrow Up`: Add one berry to your inventory - `Shift + Arrow Down`: Remove one berry from your inventory diff --git a/src/features/input/mod.rs b/src/features/input/mod.rs index 56d80ee..0365c10 100644 --- a/src/features/input/mod.rs +++ b/src/features/input/mod.rs @@ -1,4 +1,5 @@ use crate::features::{ + input::utils::mouse_to_grid, phase::messages::{NextPhaseMessage, PhaseTimerPauseMessage}, pom::messages::InvalidMoveMessage, shop::ui::open_shop, @@ -7,6 +8,8 @@ use crate::prelude::*; use bevy::input::mouse::MouseButton; use bevy::window::PrimaryWindow; +pub mod utils; + pub struct InputPlugin; impl Plugin for InputPlugin { @@ -16,11 +19,15 @@ impl Plugin for InputPlugin { app.add_systems(Update, move_click.run_if(in_state(AppState::GameScreen))); app.add_message::(); + app.add_message::(); app.add_systems( Update, interact_click.run_if(in_state(AppState::GameScreen)), ); + #[cfg(debug_assertions)] + app.add_systems(Update, debug_click.run_if(in_state(AppState::GameScreen))); + app.add_message::(); app.add_systems( Update, @@ -49,20 +56,7 @@ fn move_click( } if mouse_btn.just_pressed(MouseButton::Right) { - let (cam, cam_transform) = *camera; - - let Some(cursor_pos) = window.cursor_position() else { - return; - }; - if ui_blocks(window, cursor_pos, ui_query) { - return; - } - let Ok(world_pos) = cam.viewport_to_world(cam_transform, cursor_pos) else { - return; - }; - let Ok((x, y)) = - world_to_grid_coords(world_pos.origin, config.grid_width, config.grid_height) - else { + let Some((x, y)) = mouse_to_grid(window, camera, config, ui_query) else { return; }; @@ -72,15 +66,41 @@ fn move_click( } fn interact_click( - mut interact_messages: MessageWriter, + mut tile_click_messages: MessageWriter, mouse_btn: Res>, + keys: Res>, window: Single<&Window, With>, camera: Single<(&Camera, &GlobalTransform), With>, config: Res, phase: Res, ui_query: Query<(&ComputedNode, &GlobalTransform), With>, - // for debug - grid: ResMut, +) { + match phase.0 { + Phase::Focus { .. } => return, + _ => {} + } + + if mouse_btn.just_pressed(MouseButton::Left) { + if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) { + return; + } + let Some((x, y)) = mouse_to_grid(window, camera, config, ui_query) else { + return; + }; + + tile_click_messages.write(TileClickMessage { x, y }); + } +} + +fn debug_click( + mouse_btn: Res>, + keys: Res>, + window: Single<&Window, With>, + camera: Single<(&Camera, &GlobalTransform), With>, + config: Res, + phase: Res, + ui_query: Query<(&ComputedNode, &GlobalTransform), With>, + grid: Res, tile_query: Query<&mut TileState>, ) { match phase.0 { @@ -89,38 +109,24 @@ fn interact_click( } if mouse_btn.just_pressed(MouseButton::Left) { - let (cam, cam_transform) = *camera; - - let Some(cursor_pos) = window.cursor_position() else { - return; - }; - if ui_blocks(window, cursor_pos, ui_query) { + if !keys.pressed(KeyCode::ShiftLeft) && !keys.pressed(KeyCode::ShiftRight) { return; } - let Ok(world_pos) = cam.viewport_to_world(cam_transform, cursor_pos) else { - return; - }; - let Ok((x, y)) = - world_to_grid_coords(world_pos.origin, config.grid_width, config.grid_height) - else { + let Some((x, y)) = mouse_to_grid(window, camera, config, ui_query) else { return; }; - println!("Interact Click: ({}, {})", x, y); - interact_messages.write(InteractStartMessage { x, y }); - - if cfg!(debug_assertions) { - grid.map_tile_state( - (x, y), - |state| match state { - TileState::Unclaimed => TileState::Empty, - TileState::Empty => TileState::Occupied, - TileState::Occupied => TileState::Unclaimed, - }, - tile_query, - ) - .unwrap_or_else(|_| ()); - } + println!("Debug Toggle Click: ({}, {})", x, y); + grid.map_tile_state( + (x, y), + |state| match state { + TileState::Unclaimed => TileState::Empty, + TileState::Empty => TileState::Occupied, + TileState::Occupied => TileState::Unclaimed, + }, + tile_query, + ) + .unwrap_or_else(|_| ()); } } diff --git a/src/features/input/utils.rs b/src/features/input/utils.rs new file mode 100644 index 0000000..7f38b6e --- /dev/null +++ b/src/features/input/utils.rs @@ -0,0 +1,28 @@ +use crate::prelude::*; +use bevy::window::PrimaryWindow; + +pub fn mouse_to_grid( + window: Single<&Window, With>, + camera: Single<(&Camera, &GlobalTransform), With>, + config: Res, + ui_query: Query<(&ComputedNode, &GlobalTransform), With>, +) -> Option<(u32, u32)> { + let (cam, cam_transform) = *camera; + + let Some(cursor_pos) = window.cursor_position() else { + return None; + }; + if ui_blocks(window, cursor_pos, ui_query) { + return None; + } + let Ok(world_pos) = cam.viewport_to_world(cam_transform, cursor_pos) else { + return None; + }; + let Ok(grid_pos) = + world_to_grid_coords(world_pos.origin, config.grid_width, config.grid_height) + else { + return None; + }; + + Some(grid_pos) +} diff --git a/src/features/pom/components.rs b/src/features/pom/components.rs index 7cf4043..089438a 100644 --- a/src/features/pom/components.rs +++ b/src/features/pom/components.rs @@ -36,3 +36,8 @@ impl MovingState { ) } } + +#[derive(Component, Default)] +pub struct InteractionTarget { + pub target: Option<(u32, u32)>, +} diff --git a/src/features/pom/messages.rs b/src/features/pom/messages.rs index b823c29..60aa82c 100644 --- a/src/features/pom/messages.rs +++ b/src/features/pom/messages.rs @@ -16,3 +16,9 @@ pub struct InteractStartMessage { pub x: u32, pub y: u32, } + +#[derive(Message)] +pub struct TileClickMessage { + pub x: u32, + pub y: u32, +} diff --git a/src/features/pom/mod.rs b/src/features/pom/mod.rs index ce4a7d5..447278c 100644 --- a/src/features/pom/mod.rs +++ b/src/features/pom/mod.rs @@ -1,22 +1,33 @@ use crate::prelude::*; use components::*; -use messages::InvalidMoveMessage; +use messages::{InteractStartMessage, InvalidMoveMessage}; +use std::collections::VecDeque; use utils::find_path; +use utils::manhattan_distance; pub mod components; pub mod messages; +pub mod ui; pub mod utils; pub struct PomPlugin; impl Plugin for PomPlugin { fn build(&self, app: &mut App) { + app.add_plugins(ui::PomUiPlugin); app.add_systems(OnEnter(AppState::GameScreen), setup); app.add_systems(OnExit(AppState::GameScreen), cleanup); app.add_systems( Update, - (handle_move, move_pom, update_pom).run_if(in_state(AppState::GameScreen)), + ( + handle_move, + handle_interact, + move_pom, + update_pom, + perform_interaction, + ) + .run_if(in_state(AppState::GameScreen)), ); } } @@ -27,6 +38,7 @@ fn setup(mut commands: Commands, asset_server: Res, config: Res, grid: Res, tile_query: Query<&TileState>, - mut pom_query: Query<(&GridPosition, &mut PathQueue)>, + mut pom_query: Query<(&GridPosition, &mut PathQueue, &mut InteractionTarget)>, ) { for message in move_messages.read() { - for (grid_pos, mut path_queue) in pom_query.iter_mut() { + for (grid_pos, mut path_queue, mut interaction_target) in pom_query.iter_mut() { + // Clear any pending interaction when moving manually + interaction_target.target = None; + let grid_start = (grid_pos.x, grid_pos.y); let start = path_queue.steps.front().unwrap_or(&grid_start); let end = (message.x, message.y); @@ -78,6 +93,86 @@ fn handle_move( } } +fn handle_interact( + mut interact_messages: MessageReader, + mut pom_query: Query<(&GridPosition, &mut PathQueue, &mut InteractionTarget)>, + grid: Res, + tile_query: Query<&TileState>, +) { + for message in interact_messages.read() { + for (grid_pos, mut path_queue, mut interaction_target) in pom_query.iter_mut() { + let target_pos = (message.x, message.y); + let current_pos = (grid_pos.x, grid_pos.y); + + // If we are already adjacent to the target, just set the target and clear path + if manhattan_distance(current_pos.0, current_pos.1, target_pos.0, target_pos.1) == 1 { + path_queue.steps.clear(); + interaction_target.target = Some(target_pos); + continue; + } + + // Find a path to an adjacent tile + let neighbors = [ + (target_pos.0 as i32 + 1, target_pos.1 as i32), + (target_pos.0 as i32 - 1, target_pos.1 as i32), + (target_pos.0 as i32, target_pos.1 as i32 + 1), + (target_pos.0 as i32, target_pos.1 as i32 - 1), + ]; + + let mut best_path: Option> = None; + + for (nx, ny) in neighbors { + if nx < 0 || ny < 0 { + continue; + } + let neighbor_pos = (nx as u32, ny as u32); + + if let Some(path) = find_path(current_pos, neighbor_pos, &grid, &tile_query) { + // Pick the shortest path + if best_path.as_ref().map_or(true, |p| path.len() < p.len()) { + best_path = Some(path); + } + } + } + + if let Some(path) = best_path { + path_queue.steps = path; + interaction_target.target = Some(target_pos); + } else { + println!("Cannot reach interaction target at {:?}", target_pos); + // Don't set target if unreachable + interaction_target.target = None; + } + } + } +} + +fn perform_interaction( + mut pom_query: Query<(&GridPosition, &mut InteractionTarget, &PathQueue)>, + // grid: Res, + // mut tile_query: Query<&mut TileState>, +) { + for (pos, mut target_component, path_queue) in pom_query.iter_mut() { + if let Some(target) = target_component.target { + // Wait until movement stops + if !path_queue.steps.is_empty() { + continue; + } + + if manhattan_distance(pos.x, pos.y, target.0, target.1) == 1 { + println!( + "Performing interaction on tile ({}, {})", + target.0, target.1 + ); + + // TODO: Implement interaction logic + } + + target_component.target = None; + } + } +} + fn move_pom( time: Res