Merge branch '34-shopping-menu' into 'dev'

Implement Shoppung Menu

See merge request softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik!20
This commit is contained in:
Dominik Bernroider
2025-12-01 15:10:34 +00:00
22 changed files with 488 additions and 102 deletions

View File

@@ -41,6 +41,8 @@ cargo run
- `Shift + Enter`: Duration of the current phase is set to 3 seconds.
- `Left Mouse Button` on Tile: Rotate tile state.
- `Shift + Arrow Up`: Add one berry to your inventory
- `Shift + Arrow Down`: Remove one berry from your inventory
---

View File

@@ -2,6 +2,8 @@
"grid_width": 12,
"grid_height": 4,
"pom_speed": 1.5,
"shovel_base_price": 10,
"shovel_rate": 0.5,
"berry_seeds": [
{
"name": "Normale Samen",

View File

@@ -7,6 +7,8 @@ pub struct GameConfig {
pub grid_width: u32,
pub grid_height: u32,
pub pom_speed: f32,
pub shovel_base_price: u32,
pub shovel_rate: f32,
pub berry_seeds: Vec<BerrySeedConfig>,
}
@@ -25,6 +27,8 @@ impl Default for GameConfig {
grid_width: 12,
grid_height: 4,
pom_speed: 1.5,
shovel_base_price: 10,
shovel_rate: 0.2,
berry_seeds: vec![
BerrySeedConfig {
name: "Normale Samen".to_string(),

View File

@@ -1,6 +1,6 @@
use crate::features::inventory;
use crate::features::phase::components::TimerSettings;
use crate::features::savegame::messages::SavegameDumpMessage;
use crate::features::{inventory, shop};
use crate::prelude::*;
use components::*;
use ui::*;
@@ -41,6 +41,15 @@ fn setup(mut commands: Commands) {
children![
text_with_component(TextType::Phase, "...", 16.0, Color::WHITE),
text_with_component(TextType::Timer, "...", 16.0, Color::WHITE),
button(
shop::components::ButtonType::ShopOpen,
ButtonVariant::Secondary,
Node {
padding: UiRect::all(px(10)),
..default()
},
|color| text("Shop [P]", 16.0, color)
),
button(
inventory::components::ButtonType::InventoryOpen,
ButtonVariant::Secondary,
@@ -48,8 +57,7 @@ fn setup(mut commands: Commands) {
padding: UiRect::all(px(10)),
..default()
},
"Inventar",
16.0
|color| text("Inventar", 16.0, color)
),
button(
ButtonType::SettingsOpen,
@@ -58,8 +66,7 @@ fn setup(mut commands: Commands) {
padding: UiRect::all(px(10)),
..default()
},
"Einstellungen",
16.0
|color| text("Einstellungen", 16.0, color)
)
],
));

View File

@@ -43,8 +43,7 @@ pub fn open_settings(commands: &mut Commands) {
height: px(40),
..default()
},
"X",
24.0
|color| text("X", 24.0, color)
),
],
));
@@ -59,8 +58,7 @@ pub fn open_settings(commands: &mut Commands) {
padding: UiRect::all(px(10)),
..default()
},
"Spiel verlassen",
24.0,
|color| text("Spiel verlassen", 24.0, color)
));
parent.spawn(button(
@@ -70,8 +68,7 @@ pub fn open_settings(commands: &mut Commands) {
padding: UiRect::all(px(10)),
..default()
},
"Spiel speichern",
24.0,
|color| text("Spiel speichern", 24.0, color)
));
parent.spawn((

View File

@@ -29,8 +29,7 @@ fn timer_settings_part(input: SettingsTimerInput, amount: u32) -> impl Bundle {
width: percent(100),
..default()
},
"+",
12.0
|color| text("+", 12.0, color)
),
text_with_component(input.clone(), "--", 24.0, Color::WHITE),
button(
@@ -43,8 +42,7 @@ fn timer_settings_part(input: SettingsTimerInput, amount: u32) -> impl Bundle {
width: percent(100),
..default()
},
"-",
12.0
|color| text("-", 12.0, color)
),
],
)

View File

@@ -1,6 +1,7 @@
use crate::features::{
phase::messages::{NextPhaseMessage, PhaseTimerPauseMessage},
pom::messages::InvalidMoveMessage,
shop::ui::open_shop,
};
use crate::prelude::*;
use bevy::input::mouse::MouseButton;
@@ -28,6 +29,8 @@ impl Plugin for InputPlugin {
app.add_message::<NextPhaseMessage>();
app.add_systems(Update, next_phase.run_if(in_state(AppState::GameScreen)));
app.add_systems(Update, shop_keybind.run_if(in_state(AppState::GameScreen)));
}
}
@@ -135,3 +138,14 @@ fn next_phase(mut messages: MessageWriter<NextPhaseMessage>, keys: Res<ButtonInp
messages.write(NextPhaseMessage);
}
}
fn shop_keybind(
keys: Res<ButtonInput<KeyCode>>,
mut commands: Commands,
game_config: Res<GameConfig>,
asset_server: Res<AssetServer>,
) {
if keys.just_pressed(KeyCode::KeyP) {
open_shop(&mut commands, &game_config, &asset_server);
}
}

View File

@@ -1,15 +1,18 @@
use crate::{features::config::components::BerrySeedConfig, prelude::*};
use crate::features::config::components::BerrySeedConfig;
use crate::prelude::*;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
pub enum ItemType {
Berry,
BerrySeed { name: String },
Shovel,
}
impl ItemType {
pub fn singular(&self, game_config: &GameConfig) -> String {
match self {
ItemType::Berry => "Beere".into(),
ItemType::Shovel => "Schaufel".into(),
ItemType::BerrySeed { name } => {
let seed_config = game_config.berry_seeds.iter().find(|s| s.name == *name);
seed_config
@@ -22,6 +25,7 @@ impl ItemType {
pub fn plural(&self, game_config: &GameConfig) -> String {
match self {
ItemType::Berry => "Beeren".into(),
ItemType::Shovel => "Schaufeln".into(),
ItemType::BerrySeed { name } => {
let seed_config = game_config.berry_seeds.iter().find(|s| s.name == *name);
seed_config
@@ -36,11 +40,12 @@ impl ItemType {
ItemType::Berry => {
"Von Pflanzen erntbar. Kann im Shop zum Einkaufen benutzt werden.".into()
}
ItemType::Shovel => "Im Shop kaufbar. Schaltet ein neues Feld im Garten frei. Preis steigt bei jedem Kauf!".into(),
ItemType::BerrySeed { name } => {
let seed_config = game_config.berry_seeds.iter().find(|s| s.name == *name);
if let Some(s) = seed_config {
format!(
"Im Shop kaufbar. Kann eingepflanzt werden. Benötigt {} Fokus-Phasen zum Wachsen. Erhalte beim Ernten {} {}.",
"Im Shop kaufbar. Kann eingepflanzt werden. Nach {} Fokus-Phasen ausgewachsen. Erhalte beim Ernten {} {}.",
s.growth_stages,
s.grants,
match s.grants {
@@ -57,19 +62,27 @@ impl ItemType {
pub fn get_seed_config<'a>(&self, game_config: &'a GameConfig) -> Option<&'a BerrySeedConfig> {
match self {
ItemType::Berry => None,
ItemType::Berry | ItemType::Shovel => None,
ItemType::BerrySeed { name } => {
game_config.berry_seeds.iter().find(|s| s.name == *name)
}
}
}
pub fn get_sprite(&self, asset_server: Res<AssetServer>, game_config: &GameConfig) -> AseSlice {
pub fn get_sprite(
&self,
asset_server: &Res<AssetServer>,
game_config: &GameConfig,
) -> AseSlice {
match self {
ItemType::Berry => AseSlice {
name: "Berry".into(),
aseprite: asset_server.load("berry.aseprite"),
},
ItemType::Shovel => AseSlice {
name: "Berry".into(),
aseprite: asset_server.load("berry.aseprite"),
},
ItemType::BerrySeed { name } => {
let seed_config = game_config.berry_seeds.iter().find(|s| s.name == *name);
if let Some(s) = seed_config {
@@ -100,6 +113,76 @@ pub struct Inventory {
pub items: Vec<Entity>,
}
impl Inventory {
pub fn update_item_stack(
&mut self,
commands: &mut Commands,
items_query: &mut Query<&mut ItemStack>,
item_type_to_update: ItemType,
amount_delta: i32,
) -> bool {
let mut target_entity_index: Option<usize> = None;
let mut current_stack_amount: u32 = 0;
let mut entity_id_to_update: Option<Entity> = None;
// Try to find an existing stack of the item
for (i, &entity) in self.items.iter().enumerate() {
if let Ok(stack) = items_query.get(entity) {
if stack.item_type == item_type_to_update {
target_entity_index = Some(i);
current_stack_amount = stack.amount;
entity_id_to_update = Some(entity);
break;
}
}
}
match amount_delta {
val if val > 0 => {
// Add items
let add_amount = amount_delta as u32;
if let Some(entity) = entity_id_to_update {
if let Ok(mut stack) = items_query.get_mut(entity) {
stack.amount += add_amount;
}
} else {
// Item not found, create a new stack
let new_item_stack = ItemStack {
item_type: item_type_to_update,
amount: add_amount,
};
let id = commands.spawn(new_item_stack).id();
self.items.push(id);
}
true
}
val if val < 0 => {
// Remove items
let remove_amount = amount_delta.abs() as u32;
let Some(entity) = entity_id_to_update else {
return false; // Item not found for removal
};
if current_stack_amount < remove_amount {
return false; // Not enough items
};
if let Ok(mut stack) = items_query.get_mut(entity) {
stack.amount -= remove_amount;
if stack.amount == 0 {
commands.entity(entity).despawn();
if let Some(index) = target_entity_index {
self.items.remove(index);
}
}
}
true
}
_ => true,
}
}
}
#[derive(Component)]
pub enum RootMarker {
Inventory,

View File

@@ -11,6 +11,9 @@ impl Plugin for InventoryPlugin {
app.init_resource::<Inventory>();
app.add_systems(Update, buttons.run_if(in_state(AppState::GameScreen)));
#[cfg(debug_assertions)]
app.add_systems(Update, debug_modify_berries);
}
}
@@ -20,12 +23,13 @@ fn buttons(
itemstack_query: Query<&ItemStack>,
root_query: Query<(Entity, &RootMarker)>,
game_config: Res<GameConfig>,
asset_server: Res<AssetServer>,
) {
for (interaction, button_type) in &mut interaction_query {
match *interaction {
Interaction::Pressed => match button_type {
ButtonType::InventoryOpen => {
open_inventory(&mut commands, itemstack_query, &game_config);
open_inventory(&mut commands, itemstack_query, &game_config, &asset_server);
}
ButtonType::InventoryClose => {
for (entity, root) in root_query.iter() {
@@ -39,3 +43,21 @@ fn buttons(
}
}
}
#[cfg(debug_assertions)]
fn debug_modify_berries(
mut commands: Commands,
mut inventory: ResMut<Inventory>,
mut items: Query<&mut ItemStack>,
keys: Res<ButtonInput<KeyCode>>,
) {
if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
if keys.just_pressed(KeyCode::ArrowUp) {
println!("Adding 1 berry using debug bind");
inventory.update_item_stack(&mut commands, &mut items, ItemType::Berry, 1);
} else if keys.just_pressed(KeyCode::ArrowDown) {
println!("Removing 1 berry using debug bind");
inventory.update_item_stack(&mut commands, &mut items, ItemType::Berry, -1);
}
}
}

View File

@@ -2,7 +2,12 @@ use super::super::components::{ButtonType, RootMarker};
use crate::prelude::GameConfig;
use crate::{features::inventory::ui::list_itemstack, prelude::*};
pub fn open_inventory(commands: &mut Commands, items: Query<&ItemStack>, game_config: &Res<GameConfig>) {
pub fn open_inventory(
commands: &mut Commands,
items: Query<&ItemStack>,
game_config: &Res<GameConfig>,
asset_server: &Res<AssetServer>,
) {
commands
.spawn((
RootMarker::Inventory,
@@ -44,8 +49,7 @@ pub fn open_inventory(commands: &mut Commands, items: Query<&ItemStack>, game_co
height: px(40),
..default()
},
"X",
24.0
|color| text("X", 24.0, color)
),
],
));
@@ -58,7 +62,7 @@ pub fn open_inventory(commands: &mut Commands, items: Query<&ItemStack>, game_co
})
.with_children(|parent| {
for itemstack in items.iter() {
parent.spawn(list_itemstack(itemstack, game_config));
parent.spawn(list_itemstack(itemstack, game_config, asset_server));
}
});
});

View File

@@ -1,6 +1,10 @@
use crate::prelude::*;
pub fn list_itemstack(itemstack: &ItemStack, game_config: &GameConfig) -> impl Bundle {
pub fn list_itemstack(
itemstack: &ItemStack,
game_config: &GameConfig,
asset_server: &Res<AssetServer>,
) -> impl Bundle {
let name = match itemstack.amount {
1 => itemstack.item_type.singular(game_config),
_ => itemstack.item_type.plural(game_config),
@@ -8,21 +12,22 @@ pub fn list_itemstack(itemstack: &ItemStack, game_config: &GameConfig) -> impl B
(
Node {
width: percent(100),
padding: UiRect::all(px(4)),
..Node::hstack(px(8))
},
BackgroundColor(ButtonVariant::Secondary.normal_background()),
BorderRadius::all(px(10)),
children![
(
// Placeholder for icon
Node {
height: percent(100),
aspect_ratio: Some(1.0),
..default()
},
BackgroundColor(ButtonVariant::Secondary.hover_background()),
BorderRadius::all(px(10))
BorderRadius::all(px(10)),
itemstack.item_type.get_sprite(asset_server, game_config),
ImageNode::default()
),
(
Node {

View File

@@ -8,6 +8,7 @@ pub mod inventory;
pub mod phase;
pub mod pom;
pub mod savegame;
pub mod shop;
pub mod start_screen;
pub mod ui;
@@ -20,5 +21,6 @@ pub use inventory::InventoryPlugin;
pub use phase::PhasePlugin;
pub use pom::PomPlugin;
pub use savegame::SavegamePlugin;
pub use shop::ShopPlugin;
pub use start_screen::StartScreenPlugin;
pub use ui::UiPlugin;

View File

@@ -47,8 +47,7 @@ pub fn spawn_load_popup(commands: &mut Commands) {
height: px(40),
..default()
},
"X",
24.0
|color| text("X", 24.0, color)
)
],
));
@@ -64,63 +63,61 @@ pub fn spawn_load_popup(commands: &mut Commands) {
})
.with_children(|parent| {
for savegame in SavegamePath::list() {
parent.spawn((
Button,
ButtonType::SavegameLoad {
savegame_path: savegame.path.clone(),
},
ButtonVariant::Secondary,
Node {
width: percent(100),
height: px(80),
flex_direction: FlexDirection::Row,
column_gap: px(10.0),
padding: UiRect::horizontal(px(10.0)),
..Node::center()
},
BackgroundColor(ButtonVariant::Secondary.normal_background()),
BorderRadius::all(px(10)),
children![
(
parent.spawn(
button(
ButtonType::SavegameLoad { savegame_path: savegame.path.clone() },
ButtonVariant::Secondary,
Node {
width: percent(100),
padding: UiRect::all(px(10)),
..Node::center()
},
|color| (
Node {
width: percent(100),
height: percent(100),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
..default()
align_items: AlignItems::Center,
..Node::hstack(px(10))
},
children![
text(
format!("Spielstand {}", savegame.index + 1),
24.0,
Color::WHITE
),
text(
format!(
"Beeren: {}, Fokusphasen abgeschlossen: {}",
savegame.total_berries,
savegame.completed_focus
children![(
Node {
width: percent(100),
height: percent(100),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
..default()
},
children![
text(
format!("Spielstand {}", savegame.index + 1),
24.0,
color
),
18.0,
Color::WHITE,
),
]
),
pill_button(
ButtonType::SavegameDelete {
savegame_path: savegame.path.clone()
},
ButtonVariant::Destructive,
Node {
width: px(40),
height: px(40),
..default()
},
"X",
24.0
),
],
));
text(
format!(
"Beeren: {}, Fokusphasen abgeschlossen: {}",
savegame.total_berries,
savegame.completed_focus
),
18.0,
Color::WHITE,
),
]
),
pill_button(
ButtonType::SavegameDelete {
savegame_path: savegame.path.clone()
},
ButtonVariant::Destructive,
Node {
width: px(40),
height: px(40),
..default()
},
|color| text("X", 24.0, color)
)]
)
),
);
}
});
});

View File

@@ -0,0 +1,74 @@
use crate::prelude::*;
#[derive(Component)]
pub enum RootMarker {
Shop,
}
#[derive(Component)]
pub enum ButtonType {
ShopOpen,
ShopClose,
ShopBuyItem(ShopOffer),
}
#[derive(Clone)]
pub struct ShopOffer {
pub item: ItemStack,
pub cost: u32,
}
impl ShopOffer {
pub fn list_all(game_config: &GameConfig, tile_count: u32) -> Vec<ShopOffer> {
let mut offers = Vec::new();
for seed in &game_config.berry_seeds {
offers.push(ShopOffer {
item: ItemStack {
item_type: ItemType::BerrySeed {
name: seed.name.clone(),
},
amount: 1,
},
cost: seed.cost,
});
}
let mut shovel_cost = game_config.shovel_base_price as f32;
for _ in 0..=tile_count {
shovel_cost = shovel_cost + (game_config.shovel_rate * shovel_cost);
shovel_cost = shovel_cost.ceil();
}
offers.push(ShopOffer {
item: ItemStack {
item_type: ItemType::Shovel,
amount: 1,
},
cost: shovel_cost as u32,
});
offers
}
pub fn buy(
&self,
inventory: &mut Inventory,
commands: &mut Commands,
items: &mut Query<&mut ItemStack>,
) -> bool {
// Try to remove cost (berries)
if inventory.update_item_stack(commands, items, ItemType::Berry, -(self.cost as i32)) {
inventory.update_item_stack(
commands,
items,
self.item.item_type.clone(),
self.item.amount as i32,
);
true
} else {
false // Not enough berries
}
}
}

54
src/features/shop/mod.rs Normal file
View File

@@ -0,0 +1,54 @@
use crate::prelude::*;
use components::*;
use ui::open_shop;
pub mod components;
pub mod ui;
pub struct ShopPlugin;
impl Plugin for ShopPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, buttons.run_if(in_state(AppState::GameScreen)));
}
}
fn buttons(
mut commands: Commands,
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
root_query: Query<(Entity, &RootMarker)>,
game_config: Res<GameConfig>,
asset_server: Res<AssetServer>,
mut inventory: ResMut<Inventory>,
mut items: Query<&mut ItemStack>,
) {
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);
}
ButtonType::ShopClose => {
for (entity, root) in root_query.iter() {
match *root {
RootMarker::Shop => commands.entity(entity).despawn(),
}
}
}
ButtonType::ShopBuyItem(offer) => {
if offer.buy(&mut inventory, &mut commands, &mut items) {
// Item bought, exit the menu
for (entity, root) in root_query.iter() {
match *root {
RootMarker::Shop => commands.entity(entity).despawn(),
}
}
} else {
// Error (e.g. not enough berries)
}
}
},
_ => {}
}
}
}

View File

@@ -0,0 +1,5 @@
pub mod offer;
pub mod shop;
pub use offer::shop_offer;
pub use shop::open_shop;

View File

@@ -0,0 +1,47 @@
use super::super::components::*;
use crate::{features::inventory::ui::item::list_itemstack, prelude::*};
pub fn shop_offer(
offer: &ShopOffer,
game_config: &GameConfig,
asset_server: &Res<AssetServer>,
) -> impl Bundle {
button(
ButtonType::ShopBuyItem(offer.clone()),
ButtonVariant::Secondary,
Node::default(),
|_| {
(
Node {
width: percent(100),
align_items: AlignItems::Center,
..Node::hstack(px(10))
},
children![
list_itemstack(&offer.item, game_config, asset_server),
shop_price(offer.cost, asset_server, game_config)
],
)
},
)
}
pub fn shop_price(
price: u32,
asset_server: &Res<AssetServer>,
game_config: &GameConfig,
) -> impl Bundle {
(
Node {
align_items: AlignItems::Center,
..Node::hstack(px(0))
},
children![
text(price.to_string(), 12.0, Color::WHITE),
(
ImageNode::default(),
ItemType::Berry.get_sprite(asset_server, game_config)
)
],
)
}

View File

@@ -0,0 +1,64 @@
use super::super::components::*;
use crate::{features::shop::ui::shop_offer, prelude::*};
pub fn open_shop(
commands: &mut Commands,
game_config: &GameConfig,
asset_server: &Res<AssetServer>,
) {
// TODO: calculate tile_count
let offers = ShopOffer::list_all(game_config, 0);
commands
.spawn((
RootMarker::Shop,
Node {
position_type: PositionType::Absolute,
width: percent(100),
height: percent(100),
..Node::center()
},
ZIndex(1),
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
GlobalTransform::default(),
))
.with_children(|parent| {
parent
.spawn((
Node {
width: px(700),
padding: UiRect::all(px(20.0)),
..Node::vstack(px(20))
},
BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
BorderRadius::all(px(10.0)),
))
.with_children(|parent| {
parent.spawn((
Node {
justify_content: JustifyContent::SpaceBetween,
..Node::hstack(px(20))
},
children![
text("Shop", 40.0, Color::WHITE),
pill_button(
ButtonType::ShopClose,
ButtonVariant::Destructive,
Node {
width: px(40),
height: px(40),
..default()
},
|color| text("X", 24.0, color)
),
],
));
parent.spawn(Node::vstack(px(10))).with_children(|parent| {
for offer in offers {
parent.spawn(shop_offer(&offer, game_config, asset_server));
}
});
});
});
}

View File

@@ -34,8 +34,7 @@ fn setup(mut commands: Commands) {
padding: UiRect::all(px(10)),
..default()
},
"Spiel laden",
33.0
|color| text("Spiel laden", 33.0, color)
),
button(
ButtonType::NewGame,
@@ -45,8 +44,7 @@ fn setup(mut commands: Commands) {
padding: UiRect::all(px(10)),
..default()
},
"Neues Spiel",
33.0,
|color| text("Neues Spiel", 33.0, color)
),
button(
ButtonType::Settings,
@@ -56,8 +54,7 @@ fn setup(mut commands: Commands) {
padding: UiRect::all(px(10)),
..default()
},
"Einstellungen",
33.0
|color| text("Einstellungen", 33.0, color)
),
],
));

View File

@@ -1,14 +1,15 @@
use crate::prelude::*;
pub fn button(
pub fn button<C, R>(
button_type: impl Component,
variant: ButtonVariant,
mut node: Node,
title: impl Into<String>,
font_size: f32,
) -> impl Bundle {
child: C,
) -> impl Bundle
where
C: FnOnce(Color) -> R,
R: Bundle,
{
node.justify_content = JustifyContent::Center;
node.align_items = AlignItems::Center;
@@ -19,17 +20,20 @@ pub fn button(
node,
BackgroundColor(variant.normal_background()),
BorderRadius::all(px(10)),
children![text(title, font_size, variant.text_color())],
children![child(variant.text_color())],
)
}
pub fn pill_button(
pub fn pill_button<C, R>(
button_type: impl Component,
variant: ButtonVariant,
mut node: Node,
title: impl Into<String>,
font_size: f32,
) -> impl Bundle {
child: C,
) -> impl Bundle
where
C: FnOnce(Color) -> R,
R: Bundle,
{
node.justify_content = JustifyContent::Center;
node.align_items = AlignItems::Center;
@@ -40,7 +44,7 @@ pub fn pill_button(
node,
BackgroundColor(variant.normal_background()),
BorderRadius::MAX,
children![text(title, font_size, variant.text_color())],
children![child(variant.text_color())],
)
}

View File

@@ -33,6 +33,7 @@ fn main() {
features::SavegamePlugin,
features::UiPlugin,
features::InventoryPlugin,
features::ShopPlugin,
))
.insert_resource(config)
.run();

View File

@@ -22,7 +22,10 @@ fn test_load_valid_config() {
r#"{
"grid_width": 10,
"grid_height": 5,
"pom_speed": 2.0
"pom_speed": 2.0,
"shovel_base_price": 10,
"shovel_rate": 0.2,
"berry_seeds": []
}"#,
);