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:
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
],
|
||||
));
|
||||
|
||||
@@ -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((
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
)]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
74
src/features/shop/components.rs
Normal file
74
src/features/shop/components.rs
Normal 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
54
src/features/shop/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/features/shop/ui/mod.rs
Normal file
5
src/features/shop/ui/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod offer;
|
||||
pub mod shop;
|
||||
|
||||
pub use offer::shop_offer;
|
||||
pub use shop::open_shop;
|
||||
47
src/features/shop/ui/offer.rs
Normal file
47
src/features/shop/ui/offer.rs
Normal 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)
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
64
src/features/shop/ui/shop.rs
Normal file
64
src/features/shop/ui/shop.rs
Normal 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));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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)
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
@@ -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())],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ fn main() {
|
||||
features::SavegamePlugin,
|
||||
features::UiPlugin,
|
||||
features::InventoryPlugin,
|
||||
features::ShopPlugin,
|
||||
))
|
||||
.insert_resource(config)
|
||||
.run();
|
||||
|
||||
@@ -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": []
|
||||
}"#,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user