diff --git a/src/features/pom/actions.rs b/src/features/pom/actions.rs index d88a62e..de80406 100644 --- a/src/features/pom/actions.rs +++ b/src/features/pom/actions.rs @@ -4,6 +4,7 @@ use crate::prelude::*; pub enum InteractionAction { Plant(ItemType), Water, + Harvest, } impl InteractionAction { @@ -11,6 +12,7 @@ impl InteractionAction { match self { InteractionAction::Plant(item) => format!("Pflanze {}", item.singular(game_config)), InteractionAction::Water => "Gießen".into(), + InteractionAction::Harvest => "Ernten".into(), } } @@ -22,6 +24,7 @@ impl InteractionAction { match self { InteractionAction::Plant(item) => Some(item.get_sprite(asset_server, game_config)), InteractionAction::Water => None, + InteractionAction::Harvest => Some(ItemType::Berry.get_sprite(asset_server, game_config)), } } @@ -29,13 +32,28 @@ impl InteractionAction { tile_state: &TileState, inventory: &Inventory, item_query: Query<&ItemStack>, + game_config: &GameConfig, ) -> Vec { let mut options: Vec = vec![]; match tile_state { - TileState::Occupied { watered, .. } => { - if !*watered { - options.push(InteractionAction::Water); + TileState::Occupied { + watered, + withered, + seed, + growth_stage, + .. + } => { + if *withered { + options.push(InteractionAction::Harvest); + } else if let ItemType::BerrySeed { name } = seed { + if let Some(config) = game_config.berry_seeds.iter().find(|s| s.name == *name) { + if *growth_stage >= config.growth_stages { + options.push(InteractionAction::Harvest); + } else if !*watered { + options.push(InteractionAction::Water); + } + } } } TileState::Empty => { @@ -69,6 +87,7 @@ impl InteractionAction { inventory: &mut Inventory, item_stack_query: &mut Query<&mut ItemStack>, commands: &mut Commands, + game_config: &GameConfig, ) { let Ok(tile_entity) = grid.get_tile(pos) else { println!("Error during interaction: Couldn't get tile_entity"); @@ -123,6 +142,41 @@ impl InteractionAction { println!("Tile is not occupied, cannot water."); } } + InteractionAction::Harvest => { + if let TileState::Occupied { + seed, + withered, + growth_stage, + .. + } = &*tile_state + { + let mut can_harvest = *withered; + + if !can_harvest { + if let ItemType::BerrySeed { name } = seed { + if let Some(config) = + game_config.berry_seeds.iter().find(|s| s.name == *name) + { + if *growth_stage >= config.growth_stages { + can_harvest = true; + inventory.update_item_stack( + commands, + item_stack_query, + ItemType::Berry, + config.grants as i32, + ); + } + } + } + } + + if can_harvest { + *tile_state = TileState::Empty; + } else { + println!("Cannot harvest: not withered and not fully grown."); + } + } + } } } } diff --git a/src/features/pom/mod.rs b/src/features/pom/mod.rs index 865acae..328df1d 100644 --- a/src/features/pom/mod.rs +++ b/src/features/pom/mod.rs @@ -191,6 +191,7 @@ fn perform_interaction( mut inventory: ResMut, mut item_stack_query: Query<&mut ItemStack>, mut commands: Commands, + config: Res, ) { for (pos, mut target_component, path_queue) in pom_query.iter_mut() { if let Some(target) = target_component.target { @@ -213,6 +214,7 @@ fn perform_interaction( &mut inventory, &mut item_stack_query, &mut commands, + &config, ); } } diff --git a/src/features/pom/ui/context_menu.rs b/src/features/pom/ui/context_menu.rs index 289bb52..580dbaf 100644 --- a/src/features/pom/ui/context_menu.rs +++ b/src/features/pom/ui/context_menu.rs @@ -53,7 +53,8 @@ pub fn spawn_context_menu( let Ok(tile_state) = tile_query.get(tile_entity) else { return; }; - let options = InteractionAction::list_options(tile_state, &inventory, item_query); + let options = + InteractionAction::list_options(tile_state, &inventory, item_query, &game_config); commands .spawn(( diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..6e20f7e --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,69 @@ +use bevy::prelude::*; +use pomomon_garden::features::config::components::{BerrySeedConfig, GameConfig}; +use pomomon_garden::features::grid::components::{Grid, Tile, TileState}; +use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType}; + +pub fn setup_app( + grid_width: u32, + grid_height: u32, + initial_tile_states: &[(u32, u32, TileState)], + initial_inventory: Vec<(ItemType, u32)>, + seed_configs: Option>, +) -> App { + let mut app = App::new(); + app.add_plugins(MinimalPlugins); + app.add_plugins(AssetPlugin::default()); + + // Grid Setup + let mut grid_tiles = Vec::with_capacity(grid_width as usize); + for x in 0..grid_width { + let mut column = Vec::with_capacity(grid_height as usize); + for y in 0..grid_height { + let entity = app + .world_mut() + .spawn((Tile { x, y }, TileState::Unclaimed)) + .id(); + column.push(entity); + } + grid_tiles.push(column); + } + app.insert_resource(Grid { + width: grid_width, + height: grid_height, + tiles: grid_tiles, + }); + + for &(x, y, ref state) in initial_tile_states { + if let Ok(entity) = app.world().resource::().get_tile((x, y)) { + *app.world_mut().get_mut::(entity).unwrap() = state.clone(); + } + } + + // Inventory Setup + let mut inventory_items = Vec::new(); + for (item_type, amount) in initial_inventory { + let id = app.world_mut().spawn(ItemStack { item_type, amount }).id(); + inventory_items.push(id); + } + app.insert_resource(Inventory { + items: inventory_items, + }); + + // Game Config + let seeds = seed_configs.unwrap_or_else(|| { + vec![BerrySeedConfig { + name: "TestSeed".to_string(), + cost: 1, + grants: 1, + slice: "seed.aseprite".to_string(), + growth_stages: 2, + }] + }); + + app.insert_resource(GameConfig { + berry_seeds: seeds, + ..Default::default() + }); + + app +} diff --git a/tests/harvest.rs b/tests/harvest.rs new file mode 100644 index 0000000..7b8e68f --- /dev/null +++ b/tests/harvest.rs @@ -0,0 +1,197 @@ +use bevy::ecs::system::RunSystemOnce; +use pomomon_garden::features::config::components::{BerrySeedConfig, GameConfig}; +use pomomon_garden::features::grid::components::{Grid, TileState}; +use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType}; +use pomomon_garden::features::pom::actions::InteractionAction; +use pomomon_garden::prelude::*; + +mod common; +use common::setup_app; + +#[test] +fn test_harvest_fully_grown() { + let seed_type = ItemType::BerrySeed { + name: "TestSeed".into(), + }; + let initial_states = vec![( + 0, + 0, + TileState::Occupied { + seed: seed_type.clone(), + watered: false, + growth_stage: 2, // Max + withered: false, + dry_counter: 0, + }, + )]; + + let seed_config = BerrySeedConfig { + name: "TestSeed".into(), + cost: 1, + grants: 5, + slice: "".into(), + growth_stages: 2, + }; + + let mut app = setup_app(1, 1, &initial_states, vec![], Some(vec![seed_config])); + + let _ = app.world_mut().run_system_once( + |mut commands: Commands, + grid: Res, + mut tile_query: Query<&mut TileState>, + mut inventory: ResMut, + mut item_stack_query: Query<&mut ItemStack>, + config: Res| { + InteractionAction::Harvest.execute( + (0, 0), + &grid, + &mut tile_query, + &mut inventory, + &mut item_stack_query, + &mut commands, + &config, + ); + }, + ); + + // Check Tile State -> Empty + // Wait for commands to apply + app.update(); + + let grid = app.world().resource::(); + let tile_entity = grid.get_tile((0, 0)).unwrap(); + let tile_state = app.world().entity(tile_entity).get::().unwrap(); + assert!(matches!(tile_state, TileState::Empty)); + + // Check Inventory -> 5 Berries + let inventory = app.world().resource::(); + assert_eq!(inventory.items.len(), 1); + let stack_entity = inventory.items[0]; + let stack = app.world().entity(stack_entity).get::().unwrap(); + assert_eq!(stack.item_type, ItemType::Berry); + assert_eq!(stack.amount, 5); +} + +#[test] +fn test_harvest_withered() { + let seed_type = ItemType::BerrySeed { + name: "TestSeed".into(), + }; + let initial_states = vec![( + 0, + 0, + TileState::Occupied { + seed: seed_type.clone(), + watered: false, + growth_stage: 1, + withered: true, // Withered + dry_counter: 0, + }, + )]; + + let seed_config = BerrySeedConfig { + name: "TestSeed".into(), + cost: 1, + grants: 5, + slice: "".into(), + growth_stages: 2, + }; + + let mut app = setup_app(1, 1, &initial_states, vec![], Some(vec![seed_config])); + + let _ = app.world_mut().run_system_once( + |mut commands: Commands, + grid: Res, + mut tile_query: Query<&mut TileState>, + mut inventory: ResMut, + mut item_stack_query: Query<&mut ItemStack>, + config: Res| { + InteractionAction::Harvest.execute( + (0, 0), + &grid, + &mut tile_query, + &mut inventory, + &mut item_stack_query, + &mut commands, + &config, + ); + }, + ); + + app.update(); + + // Check Tile State -> Empty + let grid = app.world().resource::(); + let tile_entity = grid.get_tile((0, 0)).unwrap(); + let tile_state = app.world().entity(tile_entity).get::().unwrap(); + assert!(matches!(tile_state, TileState::Empty)); + + // Check Inventory -> Empty (no berries for withered) + let inventory = app.world().resource::(); + assert!(inventory.items.is_empty()); +} + +#[test] +fn test_cannot_harvest_growing() { + let seed_type = ItemType::BerrySeed { + name: "TestSeed".into(), + }; + let initial_states = vec![( + 0, + 0, + TileState::Occupied { + seed: seed_type.clone(), + watered: false, + growth_stage: 1, // Growing (Max is 2) + withered: false, + dry_counter: 0, + }, + )]; + + let seed_config = BerrySeedConfig { + name: "TestSeed".into(), + cost: 1, + grants: 5, + slice: "".into(), + growth_stages: 2, + }; + + let mut app = setup_app(1, 1, &initial_states, vec![], Some(vec![seed_config])); + + let _ = app.world_mut().run_system_once( + |mut commands: Commands, + grid: Res, + mut tile_query: Query<&mut TileState>, + mut inventory: ResMut, + mut item_stack_query: Query<&mut ItemStack>, + config: Res| { + InteractionAction::Harvest.execute( + (0, 0), + &grid, + &mut tile_query, + &mut inventory, + &mut item_stack_query, + &mut commands, + &config, + ); + }, + ); + + app.update(); + + // Check Tile State -> Still Occupied + let grid = app.world().resource::(); + let tile_entity = grid.get_tile((0, 0)).unwrap(); + let tile_state = app.world().entity(tile_entity).get::().unwrap(); + match tile_state { + TileState::Occupied { growth_stage, .. } => { + assert_eq!(*growth_stage, 1); + } + _ => panic!("Should still be occupied"), + } + + // Check Inventory -> Empty + let inventory = app.world().resource::(); + assert!(inventory.items.is_empty()); +} + diff --git a/tests/interaction.rs b/tests/interaction.rs deleted file mode 100644 index 7609fa8..0000000 --- a/tests/interaction.rs +++ /dev/null @@ -1,255 +0,0 @@ -use bevy::ecs::system::RunSystemOnce; -use pomomon_garden::features::grid::components::{Grid, Tile, TileState}; -use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType}; -use pomomon_garden::features::pom::actions::InteractionAction; -use pomomon_garden::prelude::*; - -fn setup_interaction_app( - grid_width: u32, - grid_height: u32, - initial_tile_states: &[(u32, u32, TileState)], - initial_inventory: Vec<(ItemType, u32)>, -) -> App { - let mut app = App::new(); - app.add_plugins(MinimalPlugins); - app.add_plugins(AssetPlugin::default()); // Needed for asset server if used, though we mock or avoid visuals here - - // Grid Setup - let mut grid_tiles = Vec::with_capacity(grid_width as usize); - for x in 0..grid_width { - let mut column = Vec::with_capacity(grid_height as usize); - for y in 0..grid_height { - let entity = app - .world_mut() - .spawn((Tile { x, y }, TileState::Unclaimed)) - .id(); - column.push(entity); - } - grid_tiles.push(column); - } - app.world_mut().insert_resource(Grid { - width: grid_width, - height: grid_height, - tiles: grid_tiles, - }); - - for &(x, y, ref state) in initial_tile_states { - if let Ok(entity) = app.world().resource::().get_tile((x, y)) { - *app.world_mut().get_mut::(entity).unwrap() = state.clone(); - } - } - - // Inventory Setup - let mut inventory_items = Vec::new(); - for (item_type, amount) in initial_inventory { - let id = app.world_mut().spawn(ItemStack { item_type, amount }).id(); - inventory_items.push(id); - } - app.world_mut().insert_resource(Inventory { - items: inventory_items, - }); - - app -} - -#[test] -fn test_plant_seed_interaction() { - let seed_type = ItemType::BerrySeed { - name: "TestSeed".into(), - }; - let mut app = setup_interaction_app( - 3, - 3, - &[(1, 1, TileState::Empty)], - vec![(seed_type.clone(), 1)], - ); - - // Run the interaction logic as a system or closure - let _ = app.world_mut().run_system_once( - move |grid: Res, - mut tile_query: Query<&mut TileState>, - mut inventory: ResMut, - mut item_stack_query: Query<&mut ItemStack>, - mut commands: Commands| { - let action = InteractionAction::Plant(seed_type.clone()); - action.execute( - (1, 1), - &grid, - &mut tile_query, - &mut inventory, - &mut item_stack_query, - &mut commands, - ); - }, - ); - - // Apply commands (despawns etc.) - app.update(); - - // Assert Tile State - let grid = app.world().resource::(); - let tile_entity = grid.get_tile((1, 1)).unwrap(); - let tile_state = app.world().entity(tile_entity).get::().unwrap(); - - if let TileState::Occupied { seed, .. } = tile_state { - assert_eq!( - *seed, - ItemType::BerrySeed { - name: "TestSeed".into() - } - ); - } else { - panic!("Tile should be Occupied with seed, found {:?}", tile_state); - } - - // Assert Inventory Empty - let inventory = app.world().resource::(); - // Item stack entity should be despawned or amount 0 - if !inventory.items.is_empty() { - // If the entity still exists, check amount - if let Some(entity) = inventory.items.first() { - if let Some(stack) = app.world().entity(*entity).get::() { - assert_eq!(stack.amount, 0, "Item amount should be 0"); - } else { - panic!( - "Inventory items list should be empty or point to valid 0 amount entities. Found phantom entity." - ); - } - } - } else { - assert!(inventory.items.is_empty()); - } -} - -#[test] -fn test_plant_seed_no_inventory() { - let seed_type = ItemType::BerrySeed { - name: "TestSeed".into(), - }; - let mut app = setup_interaction_app( - 3, - 3, - &[(1, 1, TileState::Empty)], - vec![], // Empty inventory - ); - - let _ = app.world_mut().run_system_once( - move |grid: Res, - mut tile_query: Query<&mut TileState>, - mut inventory: ResMut, - mut item_stack_query: Query<&mut ItemStack>, - mut commands: Commands| { - let action = InteractionAction::Plant(seed_type.clone()); - action.execute( - (1, 1), - &grid, - &mut tile_query, - &mut inventory, - &mut item_stack_query, - &mut commands, - ); - }, - ); - - app.update(); - - // Assert Tile State Unchanged - let grid = app.world().resource::(); - let tile_entity = grid.get_tile((1, 1)).unwrap(); - let tile_state = app.world().entity(tile_entity).get::().unwrap(); - - if let TileState::Empty = tile_state { - // Correct - } else { - panic!("Tile should remain Empty, found {:?}", tile_state); - } -} - -#[test] -fn test_water_crop() { - let seed_type = ItemType::BerrySeed { - name: "TestSeed".into(), - }; - let mut app = setup_interaction_app( - 3, - 3, - &[( - 1, - 1, - TileState::Occupied { - seed: seed_type.clone(), - watered: false, - growth_stage: 0, - withered: false, - dry_counter: 0, - }, - )], - vec![], - ); - - // Verify Water option is available - let _ = app.world_mut().run_system_once( - move |grid: Res, - tile_query: Query<&TileState>, - inventory: Res, - item_query: Query<&ItemStack>| { - let tile_entity = grid.get_tile((1, 1)).unwrap(); - let tile_state = tile_query.get(tile_entity).unwrap(); - let options = InteractionAction::list_options(tile_state, &inventory, item_query); - - assert!( - options.contains(&InteractionAction::Water), - "Water option should be available" - ); - }, - ); - - // Execute Water - let _ = app.world_mut().run_system_once( - move |grid: Res, - mut tile_query: Query<&mut TileState>, - mut inventory: ResMut, - mut item_stack_query: Query<&mut ItemStack>, - mut commands: Commands| { - let action = InteractionAction::Water; - action.execute( - (1, 1), - &grid, - &mut tile_query, - &mut inventory, - &mut item_stack_query, - &mut commands, - ); - }, - ); - - app.update(); - - // Assert Tile State Watered - let grid = app.world().resource::(); - let tile_entity = grid.get_tile((1, 1)).unwrap(); - let tile_state = app.world().entity(tile_entity).get::().unwrap(); - - if let TileState::Occupied { watered, .. } = tile_state { - assert!(watered, "Tile should be watered"); - } else { - panic!("Tile should be Occupied, found {:?}", tile_state); - } - - // Verify Water option is NOT available - let _ = app.world_mut().run_system_once( - move |grid: Res, - tile_query: Query<&TileState>, - inventory: Res, - item_query: Query<&ItemStack>| { - let tile_entity = grid.get_tile((1, 1)).unwrap(); - let tile_state = tile_query.get(tile_entity).unwrap(); - let options = InteractionAction::list_options(tile_state, &inventory, item_query); - - assert!( - !options.contains(&InteractionAction::Water), - "Water option should NOT be available" - ); - }, - ); -} diff --git a/tests/planting.rs b/tests/planting.rs new file mode 100644 index 0000000..1c2a0b3 --- /dev/null +++ b/tests/planting.rs @@ -0,0 +1,128 @@ +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::features::pom::actions::InteractionAction; +use pomomon_garden::prelude::*; + +mod common; +use common::setup_app; + +#[test] +fn test_plant_seed_interaction() { + let seed_type = ItemType::BerrySeed { + name: "TestSeed".into(), + }; + let mut app = setup_app( + 3, + 3, + &[(1, 1, TileState::Empty)], + vec![(seed_type.clone(), 1)], + None, + ); + + let _ = app.world_mut().run_system_once( + move |grid: Res, + mut tile_query: Query<&mut TileState>, + mut inventory: ResMut, + mut item_stack_query: Query<&mut ItemStack>, + mut commands: Commands, + game_config: Res| { + let action = InteractionAction::Plant(seed_type.clone()); + action.execute( + (1, 1), + &grid, + &mut tile_query, + &mut inventory, + &mut item_stack_query, + &mut commands, + &game_config, + ); + }, + ); + + // Apply commands (despawns etc.) + app.update(); + + // Assert Tile State + let grid = app.world().resource::(); + let tile_entity = grid.get_tile((1, 1)).unwrap(); + let tile_state = app.world().entity(tile_entity).get::().unwrap(); + + if let TileState::Occupied { seed, .. } = tile_state { + assert_eq!( + *seed, + ItemType::BerrySeed { + name: "TestSeed".into() + } + ); + } else { + panic!("Tile should be Occupied with seed, found {:?}", tile_state); + } + + // Assert Inventory Empty + let inventory = app.world().resource::(); + // Item stack entity should be despawned or amount 0 + if !inventory.items.is_empty() { + // If the entity still exists, check amount + if let Some(entity) = inventory.items.first() { + if let Some(stack) = app.world().entity(*entity).get::() { + assert_eq!(stack.amount, 0, "Item amount should be 0"); + } else { + panic!( + "Inventory items list should be empty or point to valid 0 amount entities. Found phantom entity." + ); + } + } + } else { + assert!(inventory.items.is_empty()); + } +} + +#[test] +fn test_plant_seed_no_inventory() { + let seed_type = ItemType::BerrySeed { + name: "TestSeed".into(), + }; + let mut app = setup_app( + 3, + 3, + &[(1, 1, TileState::Empty)], + vec![], // Empty inventory + None, + ); + + let _ = app.world_mut().run_system_once( + move |grid: Res, + mut tile_query: Query<&mut TileState>, + mut inventory: ResMut, + mut item_stack_query: Query<&mut ItemStack>, + mut commands: Commands, + game_config: Res| { + let action = InteractionAction::Plant(seed_type.clone()); + action.execute( + (1, 1), + &grid, + &mut tile_query, + &mut inventory, + &mut item_stack_query, + &mut commands, + &game_config, + ); + }, + ); + + app.update(); + + // Assert Tile State Unchanged + let grid = app.world().resource::(); + let tile_entity = grid.get_tile((1, 1)).unwrap(); + let tile_state = app.world().entity(tile_entity).get::().unwrap(); + + if let TileState::Empty = tile_state { + // Correct + } else { + panic!("Tile should remain Empty, found {:?}", tile_state); + } +} + diff --git a/tests/watering.rs b/tests/watering.rs new file mode 100644 index 0000000..9729cda --- /dev/null +++ b/tests/watering.rs @@ -0,0 +1,106 @@ +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::features::pom::actions::InteractionAction; +use pomomon_garden::prelude::*; + +mod common; +use common::setup_app; + +#[test] +fn test_water_crop() { + let seed_type = ItemType::BerrySeed { + name: "TestSeed".into(), + }; + let mut app = setup_app( + 3, + 3, + &[( + 1, + 1, + TileState::Occupied { + seed: seed_type.clone(), + watered: false, + growth_stage: 0, + withered: false, + dry_counter: 0, + }, + )], + vec![], + None, + ); + + // Verify Water option is available + let _ = app.world_mut().run_system_once( + move |grid: Res, + tile_query: Query<&TileState>, + inventory: Res, + item_query: Query<&ItemStack>, + game_config: Res| { + let tile_entity = grid.get_tile((1, 1)).unwrap(); + let tile_state = tile_query.get(tile_entity).unwrap(); + let options = + InteractionAction::list_options(tile_state, &inventory, item_query, &game_config); + + assert!( + options.contains(&InteractionAction::Water), + "Water option should be available" + ); + }, + ); + + // Execute Water + let _ = app.world_mut().run_system_once( + move |grid: Res, + mut tile_query: Query<&mut TileState>, + mut inventory: ResMut, + mut item_stack_query: Query<&mut ItemStack>, + mut commands: Commands, + game_config: Res| { + let action = InteractionAction::Water; + action.execute( + (1, 1), + &grid, + &mut tile_query, + &mut inventory, + &mut item_stack_query, + &mut commands, + &game_config, + ); + }, + ); + + app.update(); + + // Assert Tile State Watered + let grid = app.world().resource::(); + let tile_entity = grid.get_tile((1, 1)).unwrap(); + let tile_state = app.world().entity(tile_entity).get::().unwrap(); + + if let TileState::Occupied { watered, .. } = tile_state { + assert!(watered, "Tile should be watered"); + } else { + panic!("Tile should be Occupied, found {:?}", tile_state); + } + + // Verify Water option is NOT available + let _ = app.world_mut().run_system_once( + move |grid: Res, + tile_query: Query<&TileState>, + inventory: Res, + item_query: Query<&ItemStack>, + game_config: Res| { + let tile_entity = grid.get_tile((1, 1)).unwrap(); + let tile_state = tile_query.get(tile_entity).unwrap(); + let options = + InteractionAction::list_options(tile_state, &inventory, item_query, &game_config); + + assert!( + !options.contains(&InteractionAction::Water), + "Water option should NOT be available" + ); + }, + ); +} +