Merge branch '26-planting-crops' into 'dev'
Implement planting crops See merge request softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik!22
This commit is contained in:
BIN
assets/crop.aseprite
Normal file
BIN
assets/crop.aseprite
Normal file
Binary file not shown.
@@ -7,18 +7,23 @@ pub struct Tile {
|
|||||||
pub y: u32,
|
pub y: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component, Default, Serialize, Deserialize, Clone, Copy, Debug)]
|
#[derive(Component)]
|
||||||
|
pub struct CropVisual;
|
||||||
|
|
||||||
|
#[derive(Component, Default, Serialize, Deserialize, Clone, Debug)]
|
||||||
pub enum TileState {
|
pub enum TileState {
|
||||||
#[default]
|
#[default]
|
||||||
Unclaimed,
|
Unclaimed,
|
||||||
Empty,
|
Empty,
|
||||||
Occupied,
|
Occupied {
|
||||||
|
seed: ItemType,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TileState {
|
impl TileState {
|
||||||
pub fn is_blocking(&self) -> bool {
|
pub fn is_blocking(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
TileState::Occupied => true,
|
TileState::Occupied { .. } => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,4 +65,4 @@ impl Grid {
|
|||||||
*tile_state = mapper(&*tile_state);
|
*tile_state = mapper(&*tile_state);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use components::CropVisual;
|
||||||
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
@@ -12,10 +13,7 @@ impl Plugin for GridPlugin {
|
|||||||
app.add_systems(OnEnter(AppState::GameScreen), setup);
|
app.add_systems(OnEnter(AppState::GameScreen), setup);
|
||||||
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
||||||
|
|
||||||
app.add_systems(
|
app.add_systems(Update, update_tiles.run_if(in_state(AppState::GameScreen)));
|
||||||
Update,
|
|
||||||
update_tile_colors.run_if(in_state(AppState::GameScreen)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +43,19 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<Gam
|
|||||||
grid_height,
|
grid_height,
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn((
|
||||||
|
CropVisual,
|
||||||
|
AseSlice {
|
||||||
|
name: "Crop".into(),
|
||||||
|
aseprite: asset_server.load("crop.aseprite"),
|
||||||
|
},
|
||||||
|
Sprite::default(),
|
||||||
|
Transform::default(),
|
||||||
|
Visibility::Hidden,
|
||||||
|
ZIndex(1),
|
||||||
|
));
|
||||||
|
})
|
||||||
.id();
|
.id();
|
||||||
column.push(tile_entity);
|
column.push(tile_entity);
|
||||||
}
|
}
|
||||||
@@ -65,22 +76,32 @@ fn cleanup(mut commands: Commands, tile_query: Query<Entity, With<Tile>>) {
|
|||||||
commands.remove_resource::<Grid>();
|
commands.remove_resource::<Grid>();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_tile_colors(
|
fn update_tiles(
|
||||||
mut query: Query<(&TileState, &mut AseSlice)>,
|
mut query: Query<(&TileState, &mut AseSlice, &Children)>,
|
||||||
|
mut crop_query: Query<&mut Visibility, With<CropVisual>>,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
) {
|
) {
|
||||||
for (state, mut slice) in &mut query {
|
for (state, mut slice, children) in &mut query {
|
||||||
slice.name = match state {
|
slice.name = match state {
|
||||||
TileState::Unclaimed => "Unclaimed",
|
TileState::Unclaimed => "Unclaimed",
|
||||||
TileState::Empty => "Empty",
|
TileState::Empty => "Empty",
|
||||||
TileState::Occupied => "Occupied",
|
TileState::Occupied { .. } => "Occupied",
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
slice.aseprite = match state {
|
slice.aseprite = match state {
|
||||||
TileState::Unclaimed => asset_server.load("tiles/tile-unclaimed.aseprite"),
|
TileState::Unclaimed => asset_server.load("tiles/tile-unclaimed.aseprite"),
|
||||||
TileState::Empty => asset_server.load("tiles/tile-empty.aseprite"),
|
TileState::Empty => asset_server.load("tiles/tile-empty.aseprite"),
|
||||||
TileState::Occupied => asset_server.load("tiles/tile-occupied.aseprite"),
|
TileState::Occupied { .. } => asset_server.load("tiles/tile-occupied.aseprite"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for child in children.iter() {
|
||||||
|
if let Ok(mut visibility) = crop_query.get_mut(child) {
|
||||||
|
*visibility = match state {
|
||||||
|
TileState::Occupied { .. } => Visibility::Visible,
|
||||||
|
_ => Visibility::Hidden,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,8 +121,12 @@ fn debug_click(
|
|||||||
(x, y),
|
(x, y),
|
||||||
|state| match state {
|
|state| match state {
|
||||||
TileState::Unclaimed => TileState::Empty,
|
TileState::Unclaimed => TileState::Empty,
|
||||||
TileState::Empty => TileState::Occupied,
|
TileState::Empty => TileState::Occupied {
|
||||||
TileState::Occupied => TileState::Unclaimed,
|
seed: ItemType::BerrySeed {
|
||||||
|
name: "Debug".into(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TileState::Occupied { .. } => TileState::Unclaimed,
|
||||||
},
|
},
|
||||||
tile_query,
|
tile_query,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -114,6 +114,14 @@ pub struct Inventory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 update_item_stack(
|
pub fn update_item_stack(
|
||||||
&mut self,
|
&mut self,
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
|
|||||||
94
src/features/pom/actions.rs
Normal file
94
src/features/pom/actions.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum InteractionAction {
|
||||||
|
Plant(ItemType),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractionAction {
|
||||||
|
pub fn get_name(&self, game_config: &GameConfig) -> String {
|
||||||
|
match self {
|
||||||
|
InteractionAction::Plant(item) => format!("Pflanze {}", item.singular(game_config)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sprite(
|
||||||
|
&self,
|
||||||
|
asset_server: &Res<AssetServer>,
|
||||||
|
game_config: &GameConfig,
|
||||||
|
) -> Option<AseSlice> {
|
||||||
|
match self {
|
||||||
|
InteractionAction::Plant(item) => Some(item.get_sprite(asset_server, game_config)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_options(
|
||||||
|
tile_state: &TileState,
|
||||||
|
inventory: &Inventory,
|
||||||
|
item_query: Query<&ItemStack>,
|
||||||
|
) -> Vec<InteractionAction> {
|
||||||
|
let mut options: Vec<InteractionAction> = vec![];
|
||||||
|
|
||||||
|
for &entity in &inventory.items {
|
||||||
|
let Ok(stack) = item_query.get(entity) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if stack.amount <= 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match tile_state {
|
||||||
|
TileState::Empty => match &stack.item_type {
|
||||||
|
ItemType::BerrySeed { .. } => {
|
||||||
|
options.push(InteractionAction::Plant(stack.item_type.clone()));
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(
|
||||||
|
&self,
|
||||||
|
pos: (u32, u32),
|
||||||
|
grid: &Grid,
|
||||||
|
tile_query: &mut Query<&mut TileState>,
|
||||||
|
inventory: &mut Inventory,
|
||||||
|
item_stack_query: &mut Query<&mut ItemStack>,
|
||||||
|
commands: &mut Commands,
|
||||||
|
) {
|
||||||
|
let Ok(tile_entity) = grid.get_tile(pos) else {
|
||||||
|
println!("Error during interaction: Couldn't get tile_entity");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(mut tile_state) = tile_query.get_mut(tile_entity) else {
|
||||||
|
println!("Error during interaction: Couldn't get mut tile_state");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match self {
|
||||||
|
InteractionAction::Plant(seed_type) => {
|
||||||
|
if let TileState::Empty = *tile_state {
|
||||||
|
if inventory.update_item_stack(
|
||||||
|
commands,
|
||||||
|
item_stack_query,
|
||||||
|
seed_type.clone(),
|
||||||
|
-1,
|
||||||
|
) {
|
||||||
|
println!("Planting {:?}", seed_type);
|
||||||
|
*tile_state = TileState::Occupied {
|
||||||
|
seed: seed_type.clone(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
println!("No {:?} in inventory!", seed_type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Tile is not empty, cannot plant.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::features::pom::actions::InteractionAction;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
@@ -40,4 +41,5 @@ impl MovingState {
|
|||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
pub struct InteractionTarget {
|
pub struct InteractionTarget {
|
||||||
pub target: Option<(u32, u32)>,
|
pub target: Option<(u32, u32)>,
|
||||||
|
pub action: Option<InteractionAction>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::features::pom::actions::InteractionAction;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
@@ -15,6 +16,7 @@ pub struct InvalidMoveMessage {
|
|||||||
pub struct InteractStartMessage {
|
pub struct InteractStartMessage {
|
||||||
pub x: u32,
|
pub x: u32,
|
||||||
pub y: u32,
|
pub y: u32,
|
||||||
|
pub action: InteractionAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::collections::VecDeque;
|
|||||||
use utils::find_path;
|
use utils::find_path;
|
||||||
use utils::manhattan_distance;
|
use utils::manhattan_distance;
|
||||||
|
|
||||||
|
pub mod actions;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
@@ -71,6 +72,7 @@ fn handle_move(
|
|||||||
for (grid_pos, mut path_queue, mut interaction_target) in pom_query.iter_mut() {
|
for (grid_pos, mut path_queue, mut interaction_target) in pom_query.iter_mut() {
|
||||||
// Clear any pending interaction when moving manually
|
// Clear any pending interaction when moving manually
|
||||||
interaction_target.target = None;
|
interaction_target.target = None;
|
||||||
|
interaction_target.action = None;
|
||||||
|
|
||||||
let grid_start = (grid_pos.x, grid_pos.y);
|
let grid_start = (grid_pos.x, grid_pos.y);
|
||||||
let start = path_queue.steps.front().unwrap_or(&grid_start);
|
let start = path_queue.steps.front().unwrap_or(&grid_start);
|
||||||
@@ -108,6 +110,7 @@ fn handle_interact(
|
|||||||
if manhattan_distance(current_pos.0, current_pos.1, target_pos.0, target_pos.1) == 1 {
|
if manhattan_distance(current_pos.0, current_pos.1, target_pos.0, target_pos.1) == 1 {
|
||||||
path_queue.steps.clear();
|
path_queue.steps.clear();
|
||||||
interaction_target.target = Some(target_pos);
|
interaction_target.target = Some(target_pos);
|
||||||
|
interaction_target.action = Some(message.action.clone());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,10 +141,12 @@ fn handle_interact(
|
|||||||
if let Some(path) = best_path {
|
if let Some(path) = best_path {
|
||||||
path_queue.steps = path;
|
path_queue.steps = path;
|
||||||
interaction_target.target = Some(target_pos);
|
interaction_target.target = Some(target_pos);
|
||||||
|
interaction_target.action = Some(message.action.clone());
|
||||||
} else {
|
} else {
|
||||||
println!("Cannot reach interaction target at {:?}", target_pos);
|
println!("Cannot reach interaction target at {:?}", target_pos);
|
||||||
// Don't set target if unreachable
|
// Don't set target if unreachable
|
||||||
interaction_target.target = None;
|
interaction_target.target = None;
|
||||||
|
interaction_target.action = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,8 +154,11 @@ fn handle_interact(
|
|||||||
|
|
||||||
fn perform_interaction(
|
fn perform_interaction(
|
||||||
mut pom_query: Query<(&GridPosition, &mut InteractionTarget, &PathQueue)>,
|
mut pom_query: Query<(&GridPosition, &mut InteractionTarget, &PathQueue)>,
|
||||||
// grid: Res<Grid>,
|
grid: Res<Grid>,
|
||||||
// mut tile_query: Query<&mut TileState>,
|
mut tile_query: Query<&mut TileState>,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
|
mut commands: Commands,
|
||||||
) {
|
) {
|
||||||
for (pos, mut target_component, path_queue) in pom_query.iter_mut() {
|
for (pos, mut target_component, path_queue) in pom_query.iter_mut() {
|
||||||
if let Some(target) = target_component.target {
|
if let Some(target) = target_component.target {
|
||||||
@@ -165,10 +173,20 @@ fn perform_interaction(
|
|||||||
target.0, target.1
|
target.0, target.1
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Implement interaction logic
|
if let Some(action) = &target_component.action {
|
||||||
|
action.execute(
|
||||||
|
target,
|
||||||
|
&grid,
|
||||||
|
&mut tile_query,
|
||||||
|
&mut inventory,
|
||||||
|
&mut item_stack_query,
|
||||||
|
&mut commands,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target_component.target = None;
|
target_component.target = None;
|
||||||
|
target_component.action = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,4 +266,3 @@ fn update_pom(asset_server: Res<AssetServer>, mut query: Query<(&MovingState, &m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::features::pom::actions::InteractionAction;
|
||||||
use crate::features::ui::utils::ui_blocks;
|
use crate::features::ui::utils::ui_blocks;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use bevy::window::PrimaryWindow;
|
use bevy::window::PrimaryWindow;
|
||||||
@@ -9,7 +10,11 @@ pub enum RootMarker {
|
|||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum ButtonType {
|
pub enum ButtonType {
|
||||||
Interact { x: u32, y: u32 },
|
Interact {
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
action: InteractionAction,
|
||||||
|
},
|
||||||
Cancel,
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,6 +24,11 @@ pub fn spawn_context_menu(
|
|||||||
root_query: Query<Entity, With<RootMarker>>,
|
root_query: Query<Entity, With<RootMarker>>,
|
||||||
camera_query: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
camera_query: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||||
config: Res<GameConfig>,
|
config: Res<GameConfig>,
|
||||||
|
grid: Res<Grid>,
|
||||||
|
tile_query: Query<&TileState>,
|
||||||
|
inventory: Res<Inventory>,
|
||||||
|
item_query: Query<&ItemStack>,
|
||||||
|
game_config: Res<GameConfig>,
|
||||||
) {
|
) {
|
||||||
for message in tile_click_messages.read() {
|
for message in tile_click_messages.read() {
|
||||||
// Despawn existing menu
|
// Despawn existing menu
|
||||||
@@ -37,6 +47,14 @@ pub fn spawn_context_menu(
|
|||||||
let (camera, camera_transform) = *camera_query;
|
let (camera, camera_transform) = *camera_query;
|
||||||
|
|
||||||
if let Ok(screen_pos) = camera.world_to_viewport(camera_transform, world_pos) {
|
if let Ok(screen_pos) = camera.world_to_viewport(camera_transform, world_pos) {
|
||||||
|
let Ok(tile_entity) = grid.get_tile((message.x, message.y)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(tile_state) = tile_query.get(tile_entity) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let options = InteractionAction::list_options(tile_state, &inventory, item_query);
|
||||||
|
|
||||||
commands
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
Node {
|
Node {
|
||||||
@@ -53,18 +71,21 @@ pub fn spawn_context_menu(
|
|||||||
GlobalTransform::default(),
|
GlobalTransform::default(),
|
||||||
))
|
))
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
parent.spawn(button(
|
for option in options {
|
||||||
ButtonType::Interact {
|
parent.spawn(button(
|
||||||
x: message.x,
|
ButtonType::Interact {
|
||||||
y: message.y,
|
x: message.x,
|
||||||
},
|
y: message.y,
|
||||||
ButtonVariant::Primary,
|
action: option.clone(),
|
||||||
Node {
|
},
|
||||||
padding: UiRect::all(px(5)),
|
ButtonVariant::Primary,
|
||||||
..default()
|
Node {
|
||||||
},
|
padding: UiRect::all(px(5)),
|
||||||
|c| text("<Interact>", 20.0, c),
|
..default()
|
||||||
));
|
},
|
||||||
|
|c| text(option.clone().get_name(&game_config), 20.0, c), // TODO: add sprite
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
parent.spawn(button(
|
parent.spawn(button(
|
||||||
ButtonType::Cancel,
|
ButtonType::Cancel,
|
||||||
@@ -109,8 +130,12 @@ pub fn buttons(
|
|||||||
for (interaction, button_type) in button_query.iter_mut() {
|
for (interaction, button_type) in button_query.iter_mut() {
|
||||||
if *interaction == Interaction::Pressed {
|
if *interaction == Interaction::Pressed {
|
||||||
match button_type {
|
match button_type {
|
||||||
ButtonType::Interact { x, y } => {
|
ButtonType::Interact { x, y, action } => {
|
||||||
interact_messages.write(InteractStartMessage { x: *x, y: *y });
|
interact_messages.write(InteractStartMessage {
|
||||||
|
x: *x,
|
||||||
|
y: *y,
|
||||||
|
action: action.clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
ButtonType::Cancel => (),
|
ButtonType::Cancel => (),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ fn dump_savegame(
|
|||||||
for y in 0..grid.height {
|
for y in 0..grid.height {
|
||||||
if let Ok(entity) = grid.get_tile((x, y)) {
|
if let Ok(entity) = grid.get_tile((x, y)) {
|
||||||
if let Ok(state) = tile_query.get(entity) {
|
if let Ok(state) = tile_query.get(entity) {
|
||||||
col.push(*state);
|
col.push(state.clone());
|
||||||
} else {
|
} else {
|
||||||
col.push(TileState::Unclaimed);
|
col.push(TileState::Unclaimed);
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ fn load_savegame(
|
|||||||
if x < grid.width && y < grid.height {
|
if x < grid.width && y < grid.height {
|
||||||
if let Ok(entity) = grid.get_tile((x, y)) {
|
if let Ok(entity) = grid.get_tile((x, y)) {
|
||||||
if let Ok(mut state) = tile_query.get_mut(entity) {
|
if let Ok(mut state) = tile_query.get_mut(entity) {
|
||||||
*state = save_data.tiles[x as usize][y as usize];
|
*state = save_data.tiles[x as usize][y as usize].clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
166
tests/interaction.rs
Normal file
166
tests/interaction.rs
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
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::<Grid>().get_tile((x, y)) {
|
||||||
|
*app.world_mut().get_mut::<TileState>(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<Grid>,
|
||||||
|
mut tile_query: Query<&mut TileState>,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
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::<Grid>();
|
||||||
|
let tile_entity = grid.get_tile((1, 1)).unwrap();
|
||||||
|
let tile_state = app.world().entity(tile_entity).get::<TileState>().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::<Inventory>();
|
||||||
|
// 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::<ItemStack>() {
|
||||||
|
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<Grid>,
|
||||||
|
mut tile_query: Query<&mut TileState>,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
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::<Grid>();
|
||||||
|
let tile_entity = grid.get_tile((1, 1)).unwrap();
|
||||||
|
let tile_state = app.world().entity(tile_entity).get::<TileState>().unwrap();
|
||||||
|
|
||||||
|
if let TileState::Empty = tile_state {
|
||||||
|
// Correct
|
||||||
|
} else {
|
||||||
|
panic!("Tile should remain Empty, found {:?}", tile_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,10 +89,15 @@ fn test_find_path_simple() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_path_around_obstacle() {
|
fn test_find_path_around_obstacle() {
|
||||||
|
let obstacle: TileState = TileState::Occupied {
|
||||||
|
seed: ItemType::BerrySeed {
|
||||||
|
name: "test".into(),
|
||||||
|
},
|
||||||
|
};
|
||||||
let obstacles = vec![
|
let obstacles = vec![
|
||||||
(2, 2, TileState::Occupied),
|
(2, 2, obstacle.clone()),
|
||||||
(2, 3, TileState::Occupied),
|
(2, 3, obstacle.clone()),
|
||||||
(2, 4, TileState::Occupied),
|
(2, 4, obstacle.clone()),
|
||||||
];
|
];
|
||||||
let mut app = setup_pathfinding_app(5, 5, &obstacles);
|
let mut app = setup_pathfinding_app(5, 5, &obstacles);
|
||||||
|
|
||||||
@@ -135,12 +140,17 @@ fn test_find_path_around_obstacle() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_path_no_path() {
|
fn test_find_path_no_path() {
|
||||||
|
let obstacle: TileState = TileState::Occupied {
|
||||||
|
seed: ItemType::BerrySeed {
|
||||||
|
name: "test".into(),
|
||||||
|
},
|
||||||
|
};
|
||||||
let obstacles = vec![
|
let obstacles = vec![
|
||||||
(2, 0, TileState::Occupied),
|
(2, 0, obstacle.clone()),
|
||||||
(2, 1, TileState::Occupied),
|
(2, 1, obstacle.clone()),
|
||||||
(2, 2, TileState::Occupied),
|
(2, 2, obstacle.clone()),
|
||||||
(2, 3, TileState::Occupied),
|
(2, 3, obstacle.clone()),
|
||||||
(2, 4, TileState::Occupied),
|
(2, 4, obstacle.clone()),
|
||||||
];
|
];
|
||||||
let mut app = setup_pathfinding_app(5, 5, &obstacles);
|
let mut app = setup_pathfinding_app(5, 5, &obstacles);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user