From a96ad857a62ad03958476d86df60b96a348dd86d Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 9 Dec 2025 17:21:54 +0100 Subject: [PATCH 01/10] feat: Add shovel asset and inventory helper (#15) --- assets/shovel.aseprite | Bin 0 -> 639 bytes src/features/inventory/components.rs | 18 ++++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 assets/shovel.aseprite diff --git a/assets/shovel.aseprite b/assets/shovel.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..9c21f81ef8a970265e9986eaf81a952d26685549 GIT binary patch literal 639 zcmbg_17*c>30dyG{L4phdKu8PlA87iok8EID*?<;^0WrG**h(bp zCIZYwh_ zm2c4GZgVKt=`*m3-6;iM~WSsP6v7O1$k$%7oEz>MU? z|3Ef7gHK{*YLSAW9@v@4zF=br&d4uI%|X(^paA56Fdv9k07e85{Qv(JNU|z`{L9Ah zA0h}85oZu(sF;(Suz+VrYVM!@K1~y$yk{?O*Z+HO@%QEbUC#OkC61_FuZc-hN-jFn zKC|=e+9@-7PygR>$A8Vu7tfk9X6!WZ(Jh{MHfGaPuCAL$+SoVke35qOj4S8!$NrvX z;iFr0js{TbDdt%nC?m7Px(wgq{=jnO$ zgcM&q+cJ~Uc)m>W>^r&t-fMmNe_uY0*GxXv{!C8MnZ1{0^O{Rfuk$+VyV+#P4w+?J ccN!S4%+%v(IJ2mA=ZR;R55+K48rm`g03L(GMgRZ+ literal 0 HcmV?d00001 diff --git a/src/features/inventory/components.rs b/src/features/inventory/components.rs index 6185bac..b2ac89f 100644 --- a/src/features/inventory/components.rs +++ b/src/features/inventory/components.rs @@ -80,8 +80,8 @@ impl ItemType { aseprite: asset_server.load("berry.aseprite"), }, ItemType::Shovel => AseSlice { - name: "Berry".into(), - aseprite: asset_server.load("berry.aseprite"), + name: "Shovel".into(), + aseprite: asset_server.load("shovel.aseprite"), }, ItemType::BerrySeed { name } => { let seed_config = game_config.berry_seeds.iter().find(|s| s.name == *name); @@ -114,12 +114,14 @@ pub struct Inventory { } impl Inventory { - pub fn has_item(&self, items_query: Query<&ItemStack>) -> bool { - self.items - .iter() - .map(|entity| items_query.get(*entity).ok()) - .find(|option| option.is_some()) - .is_some() + pub fn has_item_type(&self, items_query: &Query<&ItemStack>, item_type: ItemType) -> bool { + self.items.iter().any(|&entity| { + if let Ok(stack) = items_query.get(entity) { + stack.item_type == item_type + } else { + false + } + }) } pub fn update_item_stack( From bde09ec5f260399b357594966efc2dda7a10410e Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 9 Dec 2025 17:30:36 +0100 Subject: [PATCH 02/10] feat: Enable dynamic shovel pricing calculation (#15) --- src/features/grid/components.rs | 17 ++++++++++++++++- src/features/input/mod.rs | 10 +++++++++- src/features/shop/mod.rs | 4 +++- src/features/shop/ui/shop.rs | 6 ++++-- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/features/grid/components.rs b/src/features/grid/components.rs index 571e63f..94f1e5a 100644 --- a/src/features/grid/components.rs +++ b/src/features/grid/components.rs @@ -74,4 +74,19 @@ impl Grid { *tile_state = mapper(&*tile_state); Ok(()) } -} \ No newline at end of file + + pub fn count_claimed_tiles(&self, tile_query: &Query<&TileState>) -> u32 { + self.tiles + .iter() + .flatten() + .filter(|&entity| { + if let Ok(state) = tile_query.get(*entity) { + !matches!(state, TileState::Unclaimed) + } else { + false + } + }) + .count() as u32 + } +} + diff --git a/src/features/input/mod.rs b/src/features/input/mod.rs index 3b7158b..0059c66 100644 --- a/src/features/input/mod.rs +++ b/src/features/input/mod.rs @@ -162,9 +162,17 @@ fn shop_keybind( mut commands: Commands, game_config: Res, asset_server: Res, + grid: Res, + tile_query: Query<&TileState>, ) { if keys.just_pressed(KeyCode::KeyP) { - open_shop(&mut commands, &game_config, &asset_server); + open_shop( + &mut commands, + &game_config, + &asset_server, + &grid, + &tile_query, + ); } } diff --git a/src/features/shop/mod.rs b/src/features/shop/mod.rs index ccb0cfa..b347b56 100644 --- a/src/features/shop/mod.rs +++ b/src/features/shop/mod.rs @@ -21,12 +21,14 @@ fn buttons( asset_server: Res, mut inventory: ResMut, mut items: Query<&mut ItemStack>, + grid: Res, + tile_query: Query<&TileState>, ) { for (interaction, button_type) in &mut interaction_query { match *interaction { Interaction::Pressed => match button_type { ButtonType::ShopOpen => { - open_shop(&mut commands, &game_config, &asset_server); + open_shop(&mut commands, &game_config, &asset_server, &grid, &tile_query); } ButtonType::ShopBuyItem(offer) => { if offer.buy(&mut inventory, &mut commands, &mut items) { diff --git a/src/features/shop/ui/shop.rs b/src/features/shop/ui/shop.rs index 0b260ab..4983b00 100644 --- a/src/features/shop/ui/shop.rs +++ b/src/features/shop/ui/shop.rs @@ -5,9 +5,11 @@ pub fn open_shop( commands: &mut Commands, game_config: &GameConfig, asset_server: &Res, + grid: &Grid, + tile_query: &Query<&TileState>, ) { - // TODO: calculate tile_count - let offers = ShopOffer::list_all(game_config, 0); + let tile_count = grid.count_claimed_tiles(tile_query); + let offers = ShopOffer::list_all(game_config, tile_count); spawn_popup( commands, From 8f01e0bc80c3f9415c099a4ae21f93fbda14da20 Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 9 Dec 2025 17:30:55 +0100 Subject: [PATCH 03/10] feat: Implement shovel interaction and tile highlighting (#15) --- src/features/grid/mod.rs | 55 +++++++++++++++++++++++++- src/features/input/mod.rs | 82 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/src/features/grid/mod.rs b/src/features/grid/mod.rs index d2a064f..95ecebc 100644 --- a/src/features/grid/mod.rs +++ b/src/features/grid/mod.rs @@ -1,5 +1,6 @@ use crate::prelude::*; use components::{CropVisual, WaterVisual}; +use std::collections::HashSet; pub mod components; pub mod consts; @@ -88,7 +89,10 @@ fn cleanup(mut commands: Commands, tile_query: Query>) { } fn update_tiles( - mut query: Query<(&TileState, &mut AseSlice, &Children), (With, Without)>, + mut query: Query< + (&TileState, &mut AseSlice, &Children, &Tile), + (With, Without), + >, mut crop_query: Query< (&mut Visibility, &mut Transform, &mut AseSlice), (With, Without, Without), @@ -99,8 +103,27 @@ fn update_tiles( >, asset_server: Res, game_config: Res, + inventory: Res, + item_stacks: Query<&ItemStack>, + grid: Res, + mut sprite_query: Query<&mut Sprite, With>, ) { - for (state, mut slice, children) in &mut query { + let has_shovel = inventory.has_item_type(&item_stacks, ItemType::Shovel); + + let owned_tiles: HashSet<(u32, u32)> = query + .iter() + .filter_map(|(state, _, _, tile)| { + if !matches!(state, TileState::Unclaimed) { + Some((tile.x, tile.y)) + } else { + None + } + }) + .collect(); + + for (state, mut slice, children, tile) in &mut query { + let entity = grid.get_tile((tile.x, tile.y)).unwrap(); // Get entity for sprite query + slice.name = match state { TileState::Unclaimed => "Unclaimed", TileState::Empty => "Empty", @@ -133,6 +156,34 @@ fn update_tiles( _ => Vec3::ONE, }; + let mut is_highlighted = false; + if has_shovel && matches!(state, TileState::Unclaimed) { + // Check if not on edge + if tile.x > 0 && tile.x < grid.width - 1 && tile.y > 0 && tile.y < grid.height - 1 { + // Check neighbors + let neighbors = [ + (tile.x + 1, tile.y), + (tile.x.saturating_sub(1), tile.y), + (tile.x, tile.y + 1), + (tile.x, tile.y.saturating_sub(1)), + ]; + for n in neighbors.iter() { + if owned_tiles.contains(n) { + is_highlighted = true; + break; + } + } + } + } + + if let Ok(mut sprite) = sprite_query.get_mut(entity) { + if is_highlighted { + sprite.color = Color::srgb(0.3, 1.0, 0.3); // Green tint + } else { + sprite.color = Color::WHITE; + } + } + for child in children.iter() { if let Ok((mut visibility, mut transform, mut sprite)) = crop_query.get_mut(child) { *visibility = match state { diff --git a/src/features/input/mod.rs b/src/features/input/mod.rs index 0059c66..41c7225 100644 --- a/src/features/input/mod.rs +++ b/src/features/input/mod.rs @@ -53,7 +53,13 @@ fn move_click( config: Res, phase: Res, ui_query: Query<(&ComputedNode, &GlobalTransform), With>, + inventory: Res, + 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, _ => {} @@ -78,12 +84,25 @@ fn interact_click( config: Res, phase: Res, ui_query: Query<(&ComputedNode, &GlobalTransform), With>, + mut commands: Commands, + mut inventory: ResMut, + mut item_stacks: Query<&mut ItemStack>, + grid: Res, + 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; @@ -92,10 +111,69 @@ fn interact_click( return; }; - tile_click_messages.write(TileClickMessage { x, y }); + 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 }); + } } } - fn debug_click( mouse_btn: Res>, keys: Res>, From 4b86f4bb40e4b1d77cafe0ba8c130d0f22c60ce5 Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 9 Dec 2025 17:31:42 +0100 Subject: [PATCH 04/10] chore: Update shovel price rate in config (#15) --- assets/config.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/assets/config.json b/assets/config.json index 0eaa793..1b2e028 100644 --- a/assets/config.json +++ b/assets/config.json @@ -3,7 +3,7 @@ "grid_height": 4, "pom_speed": 1.5, "shovel_base_price": 10, - "shovel_rate": 0.5, + "shovel_rate": 0.2, "berry_seeds": [ { "name": "Normale Samen", @@ -29,4 +29,3 @@ ], "wonder_event_url": "wss://pomomon.farm/ws" } - From 9437a5400a5d26e69f8160bc0fbd5fd177381167 Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 9 Dec 2025 17:48:15 +0100 Subject: [PATCH 05/10] fix: Derive `PartialEq` and `Eq` for `TileState` and remove trailing empty line --- src/features/grid/components.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/grid/components.rs b/src/features/grid/components.rs index 94f1e5a..6bbbb55 100644 --- a/src/features/grid/components.rs +++ b/src/features/grid/components.rs @@ -13,7 +13,7 @@ pub struct CropVisual; #[derive(Component)] pub struct WaterVisual; -#[derive(Component, Default, Serialize, Deserialize, Clone, Debug)] +#[derive(Component, Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum TileState { #[default] Unclaimed, @@ -89,4 +89,3 @@ impl Grid { .count() as u32 } } - From 9483c0e3b423ef1fddd9f40d81d06ff98f283d12 Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 9 Dec 2025 18:01:00 +0100 Subject: [PATCH 06/10] feat: Derive `Debug` for `ItemStack` --- src/features/inventory/components.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/inventory/components.rs b/src/features/inventory/components.rs index b2ac89f..ab3f75a 100644 --- a/src/features/inventory/components.rs +++ b/src/features/inventory/components.rs @@ -102,7 +102,7 @@ impl ItemType { } } -#[derive(Component, Serialize, Deserialize, Clone)] +#[derive(Component, Serialize, Deserialize, Clone, Debug)] pub struct ItemStack { pub item_type: ItemType, pub amount: u32, From ed18fffa43626a9f535e1d6afb0ab5ebe4bb39ed Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 9 Dec 2025 18:04:03 +0100 Subject: [PATCH 07/10] test: Add grid expansion test (#15) --- tests/expansion.rs | 116 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/expansion.rs diff --git a/tests/expansion.rs b/tests/expansion.rs new file mode 100644 index 0000000..71c9801 --- /dev/null +++ b/tests/expansion.rs @@ -0,0 +1,116 @@ +use bevy::ecs::system::RunSystemOnce; +use pomomon_garden::features::config::components::GameConfig; +use pomomon_garden::features::grid::components::{Grid, TileState}; +use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType}; +use pomomon_garden::prelude::*; + +mod common; +use common::setup_app; + +#[test] +fn test_shovel_expansion() { + let mut app = setup_app( + 5, + 5, + &[(2, 2, TileState::Empty)], // (2,2) is Empty, making it a "claimed" neighbor + vec![(ItemType::Shovel, 1)], // One shovel in inventory + None, + ); + + let target_pos = (2, 3); // Unclaimed tile next to (2,2), not on edge of 5x5 grid + + let _ = app.world_mut().run_system_once( + move | + grid: Res, + mut tile_states: Query<&mut TileState>, + mut inventory: ResMut, + mut item_stack_query: Query<&mut ItemStack>, + mut commands: Commands, + _game_config: Res // Not directly used but required by signature + | { + let (x, y) = target_pos; + + let tile_entity = match grid.get_tile((x, y)) { + Ok(entity) => entity, + Err(_) => panic!("Clicked outside grid at {:?}", (x,y)), + }; + + // Mimic the edge check from interact_click + if x == 0 || x == grid.width - 1 || y == 0 || y == grid.height - 1 { + panic!("Should not return early due to edge check for {:?} in a 5x5 grid", target_pos); + } + + let neighbors = [ + (x + 1, y), + (x.saturating_sub(1), y), + (x, y + 1), + (x, y.saturating_sub(1)), + ]; + + let mut has_claimed_neighbor = false; + for (nx, ny) in neighbors.iter() { + 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; + } + } + } + } + } + + assert!(has_claimed_neighbor, "Target tile {:?} should have a claimed neighbor", target_pos); + + let mut tile_state = match tile_states.get_mut(tile_entity) { + Ok(state) => state, + Err(_) => panic!("Failed to get mutable tile state for {:?}", target_pos), + }; + + assert!(matches!(*tile_state, TileState::Unclaimed), "Target tile {:?} should be Unclaimed initially, but was {:?}", target_pos, tile_state); + + // Execute the expansion + if inventory.update_item_stack( + &mut commands, + &mut item_stack_query, + ItemType::Shovel, + -1, + ) { + *tile_state = TileState::Empty; + } else { + panic!("Shovel not consumed or not found in inventory"); + } + }, + ); + + app.update(); // Apply commands + + // Assert Tile State + let grid = app.world().resource::(); + let tile_entity = grid.get_tile(target_pos).unwrap(); + let tile_state = app.world().entity(tile_entity).get::().unwrap(); + + assert!( + matches!(*tile_state, TileState::Empty), + "Tile (1,2) should be Empty after expansion, but was {:?}", + tile_state + ); + + // Assert Inventory + let inventory = app.world().resource::(); + let shovel_stack = inventory.items.iter().find_map(|&entity| { + let stack = app.world().entity(entity).get::()?; + if stack.item_type == ItemType::Shovel { + Some(stack) + } else { + None + } + }); + + assert!( + shovel_stack.is_none() || shovel_stack.unwrap().amount == 0, + "Shovel should have been consumed, found {:?}", + shovel_stack + ); +} From 04eb3d7c2ee66e90538d3ea386bfcfd8d8131784 Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 9 Dec 2025 18:16:02 +0100 Subject: [PATCH 08/10] chore: Update grid size --- assets/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/config.json b/assets/config.json index 1b2e028..4798184 100644 --- a/assets/config.json +++ b/assets/config.json @@ -1,6 +1,6 @@ { - "grid_width": 12, - "grid_height": 4, + "grid_width": 15, + "grid_height": 5, "pom_speed": 1.5, "shovel_base_price": 10, "shovel_rate": 0.2, From eb0b8ca91a683d35fe08aa9c46947b9b2d90f739 Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 9 Dec 2025 18:22:40 +0100 Subject: [PATCH 09/10] feat: Set (1,1) to `TileState::Empty` on new game --- src/features/grid/mod.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/features/grid/mod.rs b/src/features/grid/mod.rs index 95ecebc..4aa951e 100644 --- a/src/features/grid/mod.rs +++ b/src/features/grid/mod.rs @@ -27,13 +27,28 @@ fn setup(mut commands: Commands, asset_server: Res, config: Res "Unclaimed", + TileState::Empty => "Empty", + _ => unreachable!(), + } + .into(), + aseprite: asset_server.load(match initial_state { + TileState::Unclaimed => "tiles/tile-unclaimed.aseprite", + TileState::Empty => "tiles/tile-empty.aseprite", + _ => unreachable!(), + }), }, Sprite::default(), Transform::from_translation(grid_to_world_coords( From 50314e6c4ea23cfd5d69c172daf3e8c6341fe19b Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 9 Dec 2025 18:33:48 +0100 Subject: [PATCH 10/10] feat: Add HUD text for shovel mode --- src/features/hud/components.rs | 1 + src/features/hud/mod.rs | 85 +++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/features/hud/components.rs b/src/features/hud/components.rs index 9de7636..be49eb9 100644 --- a/src/features/hud/components.rs +++ b/src/features/hud/components.rs @@ -4,6 +4,7 @@ use crate::{features::phase::components::TimerSettings, prelude::*}; pub enum RootMarker { Status, Settings, + ShovelOverlay, } #[derive(Component)] diff --git a/src/features/hud/mod.rs b/src/features/hud/mod.rs index c780242..41937d0 100644 --- a/src/features/hud/mod.rs +++ b/src/features/hud/mod.rs @@ -16,12 +16,18 @@ impl Plugin for HudPlugin { app.add_systems(OnExit(AppState::GameScreen), cleanup); app.add_systems( Update, - (update_status, buttons, update_timer_settings).run_if(in_state(AppState::GameScreen)), + ( + update_status, + buttons, + update_timer_settings, + update_shovel_overlay_visibility, + ) + .run_if(in_state(AppState::GameScreen)), ); } } -fn setup(mut commands: Commands) { +fn setup(mut commands: Commands, game_config: Res, asset_server: Res) { commands.spawn(( RootMarker::Status, Node { @@ -61,6 +67,63 @@ fn setup(mut commands: Commands) { ) ], )); + + // Shovel Overlay + commands.spawn(( + RootMarker::ShovelOverlay, + Node { + position_type: PositionType::Absolute, + top: px(20), + left: px(0), + right: px(0), + width: percent(100), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + flex_direction: FlexDirection::Column, + row_gap: px(5), + ..default() + }, + Visibility::Hidden, + children![( + Node { + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + padding: UiRect::all(px(10)), + row_gap: px(5), + ..default() + }, + BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.7)), + BorderRadius::all(px(10)), + children![ + ( + Node { + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + column_gap: px(10), + ..default() + }, + children![ + ( + Node { + width: px(32), + height: px(32), + ..default() + }, + inventory::components::ItemType::Shovel + .get_sprite(&asset_server, &game_config), + ImageNode::default() + ), + text("Schaufel-Modus", 20.0, Color::WHITE) + ] + ), + text( + "Klicke auf ein freies Feld, um es freizuschalten.", + 14.0, + Color::WHITE + ) + ] + )], + )); } fn update_status(phase_res: Res, mut text_query: Query<(&mut Text, &TextType)>) { @@ -151,3 +214,21 @@ fn update_timer_settings( } } } + +fn update_shovel_overlay_visibility( + inventory: Res, + item_stacks: Query<&inventory::components::ItemStack>, + mut overlay_query: Query<(&RootMarker, &mut Visibility)>, +) { + let has_shovel = inventory.has_item_type(&item_stacks, inventory::components::ItemType::Shovel); + + for (marker, mut vis) in overlay_query.iter_mut() { + if let RootMarker::ShovelOverlay = marker { + *vis = if has_shovel { + Visibility::Inherited + } else { + Visibility::Hidden + }; + } + } +}