Merge branch '49-crop-growth' into 'dev'
Implement crop growth See merge request softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik!25
This commit is contained in:
@@ -21,6 +21,7 @@ pub enum TileState {
|
||||
Occupied {
|
||||
seed: ItemType,
|
||||
watered: bool,
|
||||
growth_stage: u32,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -89,9 +89,16 @@ fn cleanup(mut commands: Commands, tile_query: Query<Entity, With<Tile>>) {
|
||||
|
||||
fn update_tiles(
|
||||
mut query: Query<(&TileState, &mut AseSlice, &Children)>,
|
||||
mut crop_query: Query<&mut Visibility, (With<CropVisual>, Without<WaterVisual>)>,
|
||||
mut water_query: Query<&mut Visibility, (With<WaterVisual>, Without<CropVisual>)>,
|
||||
mut crop_query: Query<
|
||||
(&mut Visibility, &mut Transform),
|
||||
(With<CropVisual>, Without<WaterVisual>),
|
||||
>,
|
||||
mut water_query: Query<
|
||||
(&mut Visibility, &mut Transform),
|
||||
(With<WaterVisual>, Without<CropVisual>),
|
||||
>,
|
||||
asset_server: Res<AssetServer>,
|
||||
game_config: Res<GameConfig>,
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ fn debug_click(
|
||||
name: "Debug".into(),
|
||||
},
|
||||
watered: false,
|
||||
growth_stage: 0,
|
||||
},
|
||||
TileState::Occupied { .. } => TileState::Unclaimed,
|
||||
},
|
||||
|
||||
@@ -152,12 +152,13 @@ pub fn next_phase(
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_continue(
|
||||
pub fn handle_continue(
|
||||
mut messages: MessageReader<NextPhaseMessage>,
|
||||
mut phase_res: ResMut<CurrentPhase>,
|
||||
mut session_tracker: ResMut<SessionTracker>,
|
||||
settings: Res<TimerSettings>,
|
||||
mut tile_query: Query<&mut TileState>,
|
||||
game_config: Res<GameConfig>,
|
||||
) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
161
tests/growth.rs
Normal file
161
tests/growth.rs
Normal file
@@ -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::<TimerSettings>();
|
||||
app.init_resource::<SessionTracker>();
|
||||
// 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::<Grid>().get_tile((x, y)) {
|
||||
*app.world_mut().get_mut::<TileState>(entity).unwrap() = state.clone();
|
||||
}
|
||||
}
|
||||
|
||||
app.add_message::<NextPhaseMessage>();
|
||||
|
||||
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::<Grid>();
|
||||
|
||||
// Check (0,0)
|
||||
let t1 = grid.get_tile((0, 0)).unwrap();
|
||||
let s1 = app.world().entity(t1).get::<TileState>().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::<TileState>().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::<TileState>().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)"),
|
||||
}
|
||||
}
|
||||
@@ -179,6 +179,7 @@ fn test_water_crop() {
|
||||
TileState::Occupied {
|
||||
seed: seed_type.clone(),
|
||||
watered: false,
|
||||
growth_stage: 0,
|
||||
},
|
||||
)],
|
||||
vec![],
|
||||
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user