Files
pomomon-garden/src/features/input/mod.rs

295 lines
9.5 KiB
Rust

use crate::features::{
hud::ui::settings::open_settings,
input::utils::mouse_to_grid,
inventory::{components::ItemStack, ui::open_inventory},
phase::messages::{NextPhaseMessage, PhaseTimerPauseMessage},
pom::messages::InvalidMoveMessage,
shop::ui::open_shop,
ui::{messages::ClosePopupMessage, ui::popups::PopupRoot},
};
use crate::prelude::*;
use bevy::input::mouse::MouseButton;
use bevy::window::PrimaryWindow;
pub mod utils;
/// Handles user input for the game.
pub struct InputPlugin;
impl Plugin for InputPlugin {
fn build(&self, app: &mut App) {
app.add_message::<MoveMessage>();
app.add_message::<InvalidMoveMessage>();
app.add_systems(Update, move_click.run_if(in_state(AppState::GameScreen)));
app.add_message::<InteractStartMessage>();
app.add_message::<TileClickMessage>();
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::<PhaseTimerPauseMessage>();
app.add_systems(
Update,
phase_timer_pause.run_if(in_state(AppState::GameScreen)),
);
app.add_message::<NextPhaseMessage>();
app.add_systems(Update, next_phase.run_if(in_state(AppState::GameScreen)));
app.add_systems(Update, shop_keybind.run_if(in_state(AppState::GameScreen)));
app.add_systems(Update, inventory_keybind.run_if(in_state(AppState::GameScreen)));
app.add_message::<ClosePopupMessage>();
app.add_systems(Update, popup_keybind);
}
}
/// Handles right-click movement input.
fn move_click(
mut move_messages: MessageWriter<MoveMessage>,
mouse_btn: Res<ButtonInput<MouseButton>>,
window: Single<&Window, With<PrimaryWindow>>,
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
config: Res<GameConfig>,
phase: Res<CurrentPhase>,
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
inventory: Res<Inventory>,
item_stacks: Query<&ItemStack>,
) {
if inventory.has_item_type(&item_stacks, ItemType::Shovel) {
return; // Block movement if player has a shovel
}
match phase.0 {
Phase::Focus { .. } => return,
_ => {}
}
if mouse_btn.just_pressed(MouseButton::Right) {
let Some((x, y)) = mouse_to_grid(window, camera, config, ui_query) else {
return;
};
println!("Move Click: ({}, {})", x, y);
move_messages.write(MoveMessage { x, y });
}
}
/// Handles left-click interactions (tile selection, shovel).
fn interact_click(
mut tile_click_messages: MessageWriter<TileClickMessage>,
mouse_btn: Res<ButtonInput<MouseButton>>,
keys: Res<ButtonInput<KeyCode>>,
window: Single<&Window, With<PrimaryWindow>>,
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
config: Res<GameConfig>,
phase: Res<CurrentPhase>,
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
mut commands: Commands,
mut inventory: ResMut<Inventory>,
mut item_stacks: Query<&mut ItemStack>,
grid: Res<Grid>,
mut tile_states: Query<&mut TileState>,
) {
match phase.0 {
Phase::Focus { .. } => return,
_ => {}
}
let has_shovel = inventory.items.iter().any(|&entity| {
if let Ok(stack) = item_stacks.get(entity) {
stack.item_type == ItemType::Shovel
} else {
false
}
});
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;
};
if has_shovel {
// Shovel interaction logic
let tile_entity = match grid.get_tile((x, y)) {
Ok(entity) => entity,
Err(_) => return, // Clicked outside grid
};
// Before getting mutable tile_state, check neighbors with immutable borrow
let mut has_claimed_neighbor = false;
// Check if not on edge first for early exit to avoid checking neighbors outside grid.
if x == 0 || x == grid.width - 1 || y == 0 || y == grid.height - 1 {
return;
}
let neighbors = [
(x + 1, y),
(x.saturating_sub(1), y),
(x, y + 1),
(x, y.saturating_sub(1)),
];
for (nx, ny) in neighbors.iter() {
// Ensure neighbor coordinates are within grid boundaries before attempting to get tile
if *nx < grid.width && *ny < grid.height {
if let Ok(neighbor_entity) = grid.get_tile((*nx, *ny)) {
if let Ok(neighbor_state) = tile_states.get(neighbor_entity) {
if !matches!(*neighbor_state, TileState::Unclaimed) {
has_claimed_neighbor = true;
break;
}
}
}
}
}
if !has_claimed_neighbor {
return; // No claimed neighbor, cannot unlock
}
// Now get mutable tile_state, after all immutable neighbor checks are done
let mut tile_state = match tile_states.get_mut(tile_entity) {
Ok(state) => state,
Err(_) => return, // Should not happen
};
// Check if unclaimed AFTER determining neighbor status
if !matches!(*tile_state, TileState::Unclaimed) {
return;
}
if has_claimed_neighbor {
// This check is redundant due to early return above, but kept for clarity
// Unlock tile
*tile_state = TileState::Empty;
inventory.update_item_stack(&mut commands, &mut item_stacks, ItemType::Shovel, -1);
}
return; // Consume click event, prevent normal tile_click_messages
} else {
// Normal interaction
tile_click_messages.write(TileClickMessage { x, y });
}
}
}
/// Handles debug interactions (shift + left click).
fn debug_click(
mouse_btn: Res<ButtonInput<MouseButton>>,
keys: Res<ButtonInput<KeyCode>>,
window: Single<&Window, With<PrimaryWindow>>,
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
config: Res<GameConfig>,
phase: Res<CurrentPhase>,
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
grid: Res<Grid>,
tile_query: Query<&mut TileState>,
) {
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;
};
println!("Debug Toggle Click: ({}, {})", x, y);
grid.map_tile_state(
(x, y),
|state| match state {
TileState::Unclaimed => TileState::Empty,
TileState::Empty => TileState::Occupied {
seed: ItemType::BerrySeed {
name: "Debug".into(),
},
watered: false,
growth_stage: 0,
withered: false,
dry_counter: 0,
},
TileState::Occupied { .. } => TileState::Unclaimed,
},
tile_query,
)
.unwrap_or_else(|_| ());
}
}
/// Pauses/resumes the phase timer on Space press.
fn phase_timer_pause(
mut pause_messages: MessageWriter<PhaseTimerPauseMessage>,
keys: Res<ButtonInput<KeyCode>>,
) {
if keys.just_pressed(KeyCode::Space) {
pause_messages.write(PhaseTimerPauseMessage);
}
}
/// Skips to the next phase on Enter press.
fn next_phase(mut messages: MessageWriter<NextPhaseMessage>, keys: Res<ButtonInput<KeyCode>>) {
if keys.just_pressed(KeyCode::Enter) {
messages.write(NextPhaseMessage);
}
}
/// Opens the shop on 'P' press.
fn shop_keybind(
keys: Res<ButtonInput<KeyCode>>,
mut commands: Commands,
game_config: Res<GameConfig>,
asset_server: Res<AssetServer>,
grid: Res<Grid>,
tile_query: Query<&TileState>,
) {
if keys.just_pressed(KeyCode::KeyP) {
open_shop(
&mut commands,
&game_config,
&asset_server,
&grid,
&tile_query,
);
}
}
/// Opens the inventory on 'I' press.
fn inventory_keybind(
keys: Res<ButtonInput<KeyCode>>,
mut commands: Commands,
item_stacks: Query<&ItemStack>,
game_config: Res<GameConfig>,
asset_server: Res<AssetServer>,
) {
if keys.just_pressed(KeyCode::KeyI) {
open_inventory(&mut commands, item_stacks, &game_config, &asset_server);
}
}
/// Closes popups on Escape press or opens settings if no popup is open.
fn popup_keybind(
mut close_popup_messages: MessageWriter<ClosePopupMessage>,
keys: Res<ButtonInput<KeyCode>>,
popup_query: Query<Entity, With<PopupRoot>>,
mut commands: Commands,
) {
if keys.just_pressed(KeyCode::Escape) {
if !popup_query.is_empty() {
close_popup_messages.write(ClosePopupMessage);
} else {
open_settings(&mut commands);
}
}
}