295 lines
9.5 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|