From 7f134ce6962dd49448d4430a7764072e25356d50 Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 2 Dec 2025 17:04:31 +0100 Subject: [PATCH 1/4] feat: Add growth_stage to TileState --- src/features/grid/components.rs | 1 + src/features/input/mod.rs | 1 + src/features/pom/actions.rs | 4 +++- tests/interaction.rs | 1 + tests/pathfinding.rs | 2 ++ 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/features/grid/components.rs b/src/features/grid/components.rs index d2e9729..e2b20fa 100644 --- a/src/features/grid/components.rs +++ b/src/features/grid/components.rs @@ -21,6 +21,7 @@ pub enum TileState { Occupied { seed: ItemType, watered: bool, + growth_stage: u32, }, } diff --git a/src/features/input/mod.rs b/src/features/input/mod.rs index d72d0b3..e002979 100644 --- a/src/features/input/mod.rs +++ b/src/features/input/mod.rs @@ -126,6 +126,7 @@ fn debug_click( name: "Debug".into(), }, watered: false, + growth_stage: 0, }, TileState::Occupied { .. } => TileState::Unclaimed, }, diff --git a/src/features/pom/actions.rs b/src/features/pom/actions.rs index 32afa34..e63af7e 100644 --- a/src/features/pom/actions.rs +++ b/src/features/pom/actions.rs @@ -92,6 +92,7 @@ impl InteractionAction { *tile_state = TileState::Occupied { seed: seed_type.clone(), watered: false, + growth_stage: 0, }; } else { println!("No {:?} in inventory!", seed_type); @@ -101,11 +102,12 @@ impl InteractionAction { } } InteractionAction::Water => { - if let TileState::Occupied { seed, .. } = &*tile_state { + if let TileState::Occupied { seed, growth_stage, .. } = &*tile_state { println!("Watering {:?}", seed); *tile_state = TileState::Occupied { seed: seed.clone(), watered: true, + growth_stage: *growth_stage, }; } else { println!("Tile is not occupied, cannot water."); diff --git a/tests/interaction.rs b/tests/interaction.rs index 3b69d35..b657ff2 100644 --- a/tests/interaction.rs +++ b/tests/interaction.rs @@ -179,6 +179,7 @@ fn test_water_crop() { TileState::Occupied { seed: seed_type.clone(), watered: false, + growth_stage: 0, }, )], vec![], diff --git a/tests/pathfinding.rs b/tests/pathfinding.rs index 072b247..99eba5a 100644 --- a/tests/pathfinding.rs +++ b/tests/pathfinding.rs @@ -94,6 +94,7 @@ fn test_find_path_around_obstacle() { name: "Test".into(), }, watered: false, + growth_stage: 0, }; let obstacles = vec![ (2, 2, obstacle.clone()), @@ -146,6 +147,7 @@ fn test_find_path_no_path() { name: "Test".into(), }, watered: false, + growth_stage: 0, }; let obstacles = vec![ (2, 0, obstacle.clone()), From 824e4a98e4cb64db389b41c021ac70c0f408b1d2 Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 2 Dec 2025 17:04:48 +0100 Subject: [PATCH 2/4] feat: Implement crop growth logic on phase change --- src/features/phase/mod.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/features/phase/mod.rs b/src/features/phase/mod.rs index 3c4a35d..b4cb214 100644 --- a/src/features/phase/mod.rs +++ b/src/features/phase/mod.rs @@ -152,12 +152,13 @@ pub fn next_phase( } } -fn handle_continue( +pub fn handle_continue( mut messages: MessageReader, mut phase_res: ResMut, mut session_tracker: ResMut, settings: Res, mut tile_query: Query<&mut TileState>, + game_config: Res, ) { for _ in messages.read() { let entering_break = if let Phase::Finished { completed_phase } = &phase_res.0 { @@ -169,12 +170,27 @@ fn handle_continue( next_phase(&mut phase_res, &mut session_tracker, &settings); if entering_break { - println!("Resetting watered state for all crops."); + println!("Growing crops and resetting watered state."); for mut state in tile_query.iter_mut() { - if let TileState::Occupied { seed, .. } = &*state { + if let TileState::Occupied { + seed, + watered, + growth_stage, + } = &*state + { + let mut new_stage = *growth_stage; + if *watered { + if let Some(config) = seed.get_seed_config(&game_config) { + if new_stage < config.growth_stages { + new_stage += 1; + } + } + } + *state = TileState::Occupied { seed: seed.clone(), watered: false, + growth_stage: new_stage, }; } } From 715031d57015cc6c9de48fde2d8f9a40624c3923 Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 2 Dec 2025 17:04:57 +0100 Subject: [PATCH 3/4] feat: Visualise crop growth --- src/features/grid/mod.rs | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/features/grid/mod.rs b/src/features/grid/mod.rs index 3357d7a..e94af2b 100644 --- a/src/features/grid/mod.rs +++ b/src/features/grid/mod.rs @@ -89,9 +89,16 @@ fn cleanup(mut commands: Commands, tile_query: Query>) { fn update_tiles( mut query: Query<(&TileState, &mut AseSlice, &Children)>, - mut crop_query: Query<&mut Visibility, (With, Without)>, - mut water_query: Query<&mut Visibility, (With, Without)>, + mut crop_query: Query< + (&mut Visibility, &mut Transform), + (With, Without), + >, + mut water_query: Query< + (&mut Visibility, &mut Transform), + (With, Without), + >, asset_server: Res, + game_config: Res, ) { for (state, mut slice, children) in &mut query { slice.name = match state { @@ -107,18 +114,39 @@ fn update_tiles( TileState::Occupied { .. } => asset_server.load("tiles/tile-occupied.aseprite"), }; + let scale: Vec3 = match state { + TileState::Occupied { + seed, growth_stage, .. + } => { + let max_stages = seed + .get_seed_config(&game_config) + .map(|config| config.growth_stages) + .unwrap_or(0); + + if max_stages > 0 { + let progress = (*growth_stage as f32 / max_stages as f32).min(1.0); + Vec3::splat(0.3 + (progress * 0.7)) + } else { + Vec3::ONE + } + } + _ => Vec3::ONE, + }; + for child in children.iter() { - if let Ok(mut visibility) = crop_query.get_mut(child) { + if let Ok((mut visibility, mut transform)) = crop_query.get_mut(child) { *visibility = match state { TileState::Occupied { .. } => Visibility::Visible, _ => Visibility::Hidden, }; + transform.scale = scale; } - if let Ok(mut visibility) = water_query.get_mut(child) { + if let Ok((mut visibility, mut transform)) = water_query.get_mut(child) { *visibility = match state { TileState::Occupied { watered: true, .. } => Visibility::Visible, _ => Visibility::Hidden, }; + transform.scale = scale; } } } From 9ddd24c2820db9b75ac609707b345b786d1cf539 Mon Sep 17 00:00:00 2001 From: demenik Date: Tue, 2 Dec 2025 17:05:43 +0100 Subject: [PATCH 4/4] test: Add growth mechanics tests --- tests/growth.rs | 161 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/growth.rs diff --git a/tests/growth.rs b/tests/growth.rs new file mode 100644 index 0000000..85ed8fb --- /dev/null +++ b/tests/growth.rs @@ -0,0 +1,161 @@ +use bevy::ecs::system::RunSystemOnce; +use pomomon_garden::features::config::components::{BerrySeedConfig, GameConfig}; +use pomomon_garden::features::grid::components::{Grid, Tile, TileState}; +use pomomon_garden::features::inventory::components::ItemType; +use pomomon_garden::features::phase::components::{ + CurrentPhase, Phase, SessionTracker, TimerSettings, +}; +use pomomon_garden::features::phase::handle_continue; +use pomomon_garden::features::phase::messages::NextPhaseMessage; +use pomomon_garden::prelude::*; + +fn setup_growth_app( + grid_width: u32, + grid_height: u32, + initial_tile_states: &[(u32, u32, TileState)], +) -> App { + let mut app = App::new(); + app.add_plugins(MinimalPlugins); + app.add_plugins(AssetPlugin::default()); + + app.init_resource::(); + app.init_resource::(); + // Initialize phase as Finished(Focus) to trigger growth on continue + app.insert_resource(CurrentPhase(Phase::Finished { + completed_phase: Box::new(Phase::Focus { duration: 25.0 }), + })); + + // GameConfig with known seeds + app.insert_resource(GameConfig { + berry_seeds: vec![BerrySeedConfig { + name: "FastSeed".into(), + cost: 1, + grants: 1, + slice: "".into(), + growth_stages: 2, + }], + ..Default::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(); + } + } + + app.add_message::(); + + app +} + +#[test] +fn test_crop_growth_logic() { + let seed_type = ItemType::BerrySeed { + name: "FastSeed".into(), + }; + + // Scenario: + // (0,0): Watered, Stage 0 -> Should grow to 1, become unwatered + // (1,0): Unwatered, Stage 0 -> Should NOT grow, stay unwatered + // (2,0): Watered, Stage 2 (Max) -> Should NOT grow, become unwatered + + let initial_states = vec![ + ( + 0, + 0, + TileState::Occupied { + seed: seed_type.clone(), + watered: true, + growth_stage: 0, + }, + ), + ( + 1, + 0, + TileState::Occupied { + seed: seed_type.clone(), + watered: false, + growth_stage: 0, + }, + ), + ( + 2, + 0, + TileState::Occupied { + seed: seed_type.clone(), + watered: true, + growth_stage: 2, // Max + }, + ), + ]; + + let mut app = setup_growth_app(3, 1, &initial_states); + + app.world_mut().write_message(NextPhaseMessage); + let _ = app.world_mut().run_system_once(handle_continue); + let grid = app.world().resource::(); + + // Check (0,0) + let t1 = grid.get_tile((0, 0)).unwrap(); + let s1 = app.world().entity(t1).get::().unwrap(); + match s1 { + TileState::Occupied { + watered, + growth_stage, + .. + } => { + assert_eq!(*growth_stage, 1, "(0,0) should grow to stage 1"); + assert_eq!(*watered, false, "(0,0) should be unwatered"); + } + _ => panic!("Invalid state at (0,0)"), + } + + // Check (1,0) + let t2 = grid.get_tile((1, 0)).unwrap(); + let s2 = app.world().entity(t2).get::().unwrap(); + match s2 { + TileState::Occupied { + watered, + growth_stage, + .. + } => { + assert_eq!(*growth_stage, 0, "(1,0) should stay at stage 0"); + assert_eq!(*watered, false, "(1,0) should be unwatered"); + } + _ => panic!("Invalid state at (1,0)"), + } + + // Check (2,0) + let t3 = grid.get_tile((2, 0)).unwrap(); + let s3 = app.world().entity(t3).get::().unwrap(); + match s3 { + TileState::Occupied { + watered, + growth_stage, + .. + } => { + assert_eq!(*growth_stage, 2, "(2,0) should stay at stage 2 (max)"); + assert_eq!(*watered, false, "(2,0) should be unwatered"); + } + _ => panic!("Invalid state at (2,0)"), + } +}