diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..7b414d0 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,32 @@ +image: "rust:latest" +stages: + - build + - test +cache: + key: "$CI_COMMIT_REF_SLUG" + paths: + - .cargo/ + - target/ +.only_on_main_rule: &only_on_main_rule + rules: + - if: $CI_COMMIT_REF_SLUG == "main" +.install_linux_dependencies: &install_linux_dependencies + before_script: + - apt-get update -yq + - apt-get install -yq g++ pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 +linux_build: + stage: build + !!merge <<: *only_on_main_rule + !!merge <<: *install_linux_dependencies + script: + - cargo build --release --target x86_64-unknown-linux-gnu + artifacts: + paths: + - target/x86_64-unknown-linux-gnu/release/pomomon-garden + name: "pomomon-garden-linux-x86_64-$CI_COMMIT_SHORT_SHA" + expire_in: "2 weeks" +run_tests: + stage: test + !!merge <<: *install_linux_dependencies + script: + - cargo test --verbose diff --git a/Cargo.lock b/Cargo.lock index 21c0c78..a56f128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3663,6 +3663,7 @@ dependencies = [ "directories", "serde", "serde_json", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c345354..411a34b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,6 @@ bevy_dev_tools = "0.17.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" directories = "6.0" + +[dev-dependencies] +uuid = "1.18.1" diff --git a/README.md b/README.md index 88420ea..b77f1be 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pomomon Garden ![Sleeping Pom](pom-sleep.gif) -Pomomon Garden is a Pomodoro game created during ["Sopra" (Softwareprojekt)](https://www.uni-ulm.de/in/sgi/lehre/softwaojekt-sopra-1/) WiSe 25/26 at the University of Ulm. +Pomomon Garden is a Pomodoro game created during ["Sopra" (Softwareprojekt)](https://www.uni-ulm.de/in/sgi/lehre/softwareprojekt-sopra-1/) WiSe 25/26 at the University of Ulm. It uses the [ECS](https://en.wikipedia.org/wiki/Entity_component_system)-based Rust game engine called [Bevy](https://bevy.org/). --- @@ -40,10 +40,14 @@ cargo run ### Hidden binds (Only available in the debug build) - `Shift + Enter`: Duration of the current phase is set to 3 seconds. -- `Left Mouse Button` on Tile: Rotate tile state. +- `Shift + Left Mouse Button` on Tile: Toggle tile state. +- `Shift + Arrow Up`: Add one berry to your inventory +- `Shift + Arrow Down`: Remove one berry from your inventory --- ## Licensing This project is released under the terms of the [MIT License](LICENSE). + +The font used is called Jersey 10. It is protected under the [SIL OPEN FONT LICENSE Version 1.1](assets/fonts/Jersey10.LICENSE). diff --git a/assets/berry.aseprite b/assets/berry.aseprite new file mode 100644 index 0000000..2675e6e Binary files /dev/null and b/assets/berry.aseprite differ diff --git a/assets/config.json b/assets/config.json index 2695fb3..56bc933 100644 --- a/assets/config.json +++ b/assets/config.json @@ -1,5 +1,30 @@ { "grid_width": 12, "grid_height": 4, - "pom_speed": 1.5 -} + "pom_speed": 1.5, + "shovel_base_price": 10, + "shovel_rate": 0.5, + "berry_seeds": [ + { + "name": "Normale Samen", + "cost": 1, + "grants": 2, + "slice": "Seed1", + "growth_stages": 2 + }, + { + "name": "Super-Samen", + "cost": 3, + "grants": 9, + "slice": "Seed2", + "growth_stages": 4 + }, + { + "name": "Zauber-Samen", + "cost": 5, + "grants": 20, + "slice": "Seed3", + "growth_stages": 6 + } + ] +} \ No newline at end of file diff --git a/assets/crop.aseprite b/assets/crop.aseprite new file mode 100644 index 0000000..08ecc00 Binary files /dev/null and b/assets/crop.aseprite differ diff --git a/assets/fonts/Jersey10-Regular.ttf b/assets/fonts/Jersey10-Regular.ttf new file mode 100644 index 0000000..42ac920 Binary files /dev/null and b/assets/fonts/Jersey10-Regular.ttf differ diff --git a/assets/fonts/Jersey10.LICENSE b/assets/fonts/Jersey10.LICENSE new file mode 100644 index 0000000..befc2e5 --- /dev/null +++ b/assets/fonts/Jersey10.LICENSE @@ -0,0 +1,93 @@ +Copyright 2023 The Soft Type Project Authors (https://github.com/scfried/soft-type-jersey) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/seed.aseprite b/assets/seed.aseprite new file mode 100644 index 0000000..2bf5839 Binary files /dev/null and b/assets/seed.aseprite differ diff --git a/src/features/config/components.rs b/src/features/config/components.rs index 94fb4fc..6533682 100644 --- a/src/features/config/components.rs +++ b/src/features/config/components.rs @@ -7,6 +7,18 @@ 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, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct BerrySeedConfig { + pub name: String, + pub cost: u32, + pub grants: u32, + pub slice: String, + pub growth_stages: u32, } impl Default for GameConfig { @@ -15,13 +27,42 @@ 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(), + cost: 1, + grants: 2, + slice: "Seed1".to_string(), + growth_stages: 2, + }, + BerrySeedConfig { + name: "Super-Samen".to_string(), + cost: 3, + grants: 9, + slice: "Seed2".to_string(), + growth_stages: 4, + }, + BerrySeedConfig { + name: "Zauber-Samen".to_string(), + cost: 5, + grants: 20, + slice: "Seed3".to_string(), + growth_stages: 6, + }, + ], } } } impl GameConfig { pub fn read_config() -> Option { - let file = File::open("assets/config.json").ok()?; + Self::read_from_path(std::path::Path::new("assets/config.json")) + } + + pub fn read_from_path(path: &std::path::Path) -> Option { + let file = File::open(path).ok()?; let reader = BufReader::new(file); serde_json::from_reader(reader).ok() } diff --git a/src/features/grid/components.rs b/src/features/grid/components.rs index 6875de2..571e63f 100644 --- a/src/features/grid/components.rs +++ b/src/features/grid/components.rs @@ -7,18 +7,32 @@ pub struct Tile { pub y: u32, } -#[derive(Component, Default, Serialize, Deserialize, Clone, Copy, Debug)] +#[derive(Component)] +pub struct CropVisual; + +#[derive(Component)] +pub struct WaterVisual; + +#[derive(Component, Default, Serialize, Deserialize, Clone, Debug)] pub enum TileState { #[default] Unclaimed, Empty, - Occupied, + Occupied { + seed: ItemType, + watered: bool, + growth_stage: u32, + #[serde(default)] + withered: bool, + #[serde(default)] + dry_counter: u8, + }, } impl TileState { pub fn is_blocking(&self) -> bool { match self { - TileState::Occupied => true, + TileState::Occupied { .. } => true, _ => false, } } @@ -60,4 +74,4 @@ impl Grid { *tile_state = mapper(&*tile_state); Ok(()) } -} +} \ No newline at end of file diff --git a/src/features/grid/mod.rs b/src/features/grid/mod.rs index 1f06f99..d2a064f 100644 --- a/src/features/grid/mod.rs +++ b/src/features/grid/mod.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use components::{CropVisual, WaterVisual}; pub mod components; pub mod consts; @@ -12,10 +13,7 @@ impl Plugin for GridPlugin { app.add_systems(OnEnter(AppState::GameScreen), setup); app.add_systems(OnExit(AppState::GameScreen), cleanup); - app.add_systems( - Update, - update_tile_colors.run_if(in_state(AppState::GameScreen)), - ); + app.add_systems(Update, update_tiles.run_if(in_state(AppState::GameScreen))); } } @@ -45,6 +43,30 @@ fn setup(mut commands: Commands, asset_server: Res, config: Res>) { commands.remove_resource::(); } -fn update_tile_colors( - mut query: Query<(&TileState, &mut AseSlice)>, +fn update_tiles( + mut query: Query<(&TileState, &mut AseSlice, &Children), (With, Without)>, + mut crop_query: Query< + (&mut Visibility, &mut Transform, &mut AseSlice), + (With, Without, Without), + >, + mut water_query: Query< + (&mut Visibility, &mut Transform), + (With, Without), + >, asset_server: Res, + game_config: Res, ) { - for (state, mut slice) in &mut query { + for (state, mut slice, children) in &mut query { slice.name = match state { TileState::Unclaimed => "Unclaimed", TileState::Empty => "Empty", - TileState::Occupied => "Occupied", + TileState::Occupied { .. } => "Occupied", } .into(); slice.aseprite = match state { TileState::Unclaimed => asset_server.load("tiles/tile-unclaimed.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"), }; + + 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, mut transform, mut sprite)) = crop_query.get_mut(child) { + *visibility = match state { + TileState::Occupied { .. } => Visibility::Visible, + _ => Visibility::Hidden, + }; + transform.scale = scale; + + if let TileState::Occupied { withered: true, .. } = state { + sprite.name = "Wither".into(); + } else { + sprite.name = "Crop".into(); + } + } + 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; + } + } } } diff --git a/src/features/grid/utils.rs b/src/features/grid/utils.rs index 3c032da..95c722e 100644 --- a/src/features/grid/utils.rs +++ b/src/features/grid/utils.rs @@ -1,3 +1,4 @@ +use super::errors::GridError; use crate::prelude::*; pub fn grid_start_x(grid_width: u32) -> f32 { @@ -8,24 +9,25 @@ pub fn grid_start_y(grid_height: u32) -> f32 { -(grid_height as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0 } -pub fn world_to_grid_coords(world_pos: Vec3, grid_width: u32, grid_height: u32) -> (u32, u32) { +pub fn world_to_grid_coords( + world_pos: Vec3, + grid_width: u32, + grid_height: u32, +) -> Result<(u32, u32), GridError> { let start_x = grid_start_x(grid_width); let start_y = grid_start_y(grid_height); let x = ((world_pos.x - start_x + TILE_SIZE / 2.0) / TILE_SIZE).floor(); let y = ((world_pos.y - start_y + TILE_SIZE / 2.0) / TILE_SIZE).floor(); - let mut x_u32 = x as u32; - let mut y_u32 = y as u32; - - if x_u32 >= grid_width { - x_u32 = grid_width - 1; - } - if y_u32 >= grid_height { - y_u32 = grid_height - 1; + if x >= grid_width as f32 || y >= grid_height as f32 || x < 0.0 || y < 0.0 { + return Err(GridError::OutOfBounds { + x: x as i32, + y: x as i32, + }); } - (x_u32, y_u32) + Ok((x as u32, y as u32)) } pub fn grid_to_world_coords( diff --git a/src/features/hud/mod.rs b/src/features/hud/mod.rs index 6091430..875c501 100644 --- a/src/features/hud/mod.rs +++ b/src/features/hud/mod.rs @@ -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) ) ], )); diff --git a/src/features/hud/ui/settings.rs b/src/features/hud/ui/settings.rs index dd8868d..cb82cc4 100644 --- a/src/features/hud/ui/settings.rs +++ b/src/features/hud/ui/settings.rs @@ -14,6 +14,7 @@ pub fn open_settings(commands: &mut Commands) { }, ZIndex(1), BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)), + GlobalTransform::default(), )) .with_children(|parent| { parent @@ -42,8 +43,7 @@ pub fn open_settings(commands: &mut Commands) { height: px(40), ..default() }, - "X", - 24.0 + |color| text("X", 24.0, color) ), ], )); @@ -58,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( @@ -69,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(( diff --git a/src/features/hud/ui/timer_settings.rs b/src/features/hud/ui/timer_settings.rs index 49fce78..136b391 100644 --- a/src/features/hud/ui/timer_settings.rs +++ b/src/features/hud/ui/timer_settings.rs @@ -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) ), ], ) diff --git a/src/features/input/mod.rs b/src/features/input/mod.rs index ce12444..8ed6757 100644 --- a/src/features/input/mod.rs +++ b/src/features/input/mod.rs @@ -1,11 +1,15 @@ use crate::features::{ + input::utils::mouse_to_grid, phase::messages::{NextPhaseMessage, PhaseTimerPauseMessage}, pom::messages::InvalidMoveMessage, + shop::ui::open_shop, }; use crate::prelude::*; use bevy::input::mouse::MouseButton; use bevy::window::PrimaryWindow; +pub mod utils; + pub struct InputPlugin; impl Plugin for InputPlugin { @@ -15,11 +19,15 @@ impl Plugin for InputPlugin { app.add_systems(Update, move_click.run_if(in_state(AppState::GameScreen))); app.add_message::(); + app.add_message::(); app.add_systems( Update, interact_click.run_if(in_state(AppState::GameScreen)), ); + #[cfg(debug_assertions)] + app.add_systems(Update, debug_click.run_if(in_state(AppState::GameScreen))); + app.add_message::(); app.add_systems( Update, @@ -28,6 +36,8 @@ impl Plugin for InputPlugin { app.add_message::(); app.add_systems(Update, next_phase.run_if(in_state(AppState::GameScreen))); + + app.add_systems(Update, shop_keybind.run_if(in_state(AppState::GameScreen))); } } @@ -38,6 +48,7 @@ fn move_click( camera: Single<(&Camera, &GlobalTransform), With>, config: Res, phase: Res, + ui_query: Query<(&ComputedNode, &GlobalTransform), With>, ) { match phase.0 { Phase::Focus { .. } => return, @@ -45,15 +56,9 @@ fn move_click( } if mouse_btn.just_pressed(MouseButton::Right) { - let (cam, cam_transform) = *camera; - - let Some(cursor_pos) = window.cursor_position() else { + let Some((x, y)) = mouse_to_grid(window, camera, config, ui_query) else { return; }; - let Ok(world_pos) = cam.viewport_to_world(cam_transform, cursor_pos) else { - return; - }; - let (x, y) = world_to_grid_coords(world_pos.origin, config.grid_width, config.grid_height); println!("Move Click: ({}, {})", x, y); move_messages.write(MoveMessage { x, y }); @@ -61,14 +66,41 @@ fn move_click( } fn interact_click( - mut interact_messages: MessageWriter, + mut tile_click_messages: MessageWriter, mouse_btn: Res>, + keys: Res>, window: Single<&Window, With>, camera: Single<(&Camera, &GlobalTransform), With>, config: Res, phase: Res, - // for debug - grid: ResMut, + ui_query: Query<(&ComputedNode, &GlobalTransform), With>, +) { + match phase.0 { + Phase::Focus { .. } => return, + _ => {} + } + + if mouse_btn.just_pressed(MouseButton::Left) { + if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) { + return; + } + let Some((x, y)) = mouse_to_grid(window, camera, config, ui_query) else { + return; + }; + + tile_click_messages.write(TileClickMessage { x, y }); + } +} + +fn debug_click( + mouse_btn: Res>, + keys: Res>, + window: Single<&Window, With>, + camera: Single<(&Camera, &GlobalTransform), With>, + config: Res, + phase: Res, + ui_query: Query<(&ComputedNode, &GlobalTransform), With>, + grid: Res, tile_query: Query<&mut TileState>, ) { match phase.0 { @@ -77,31 +109,32 @@ fn interact_click( } if mouse_btn.just_pressed(MouseButton::Left) { - let (cam, cam_transform) = *camera; - - let Some(cursor_pos) = window.cursor_position() else { + if !keys.pressed(KeyCode::ShiftLeft) && !keys.pressed(KeyCode::ShiftRight) { return; - }; - let Ok(world_pos) = cam.viewport_to_world(cam_transform, cursor_pos) else { - return; - }; - let (x, y) = world_to_grid_coords(world_pos.origin, config.grid_width, config.grid_height); - - println!("Interact Click: ({}, {})", x, y); - interact_messages.write(InteractStartMessage { x, y }); - - if cfg!(debug_assertions) { - grid.map_tile_state( - (x, y), - |state| match state { - TileState::Unclaimed => TileState::Empty, - TileState::Empty => TileState::Occupied, - TileState::Occupied => TileState::Unclaimed, - }, - tile_query, - ) - .unwrap(); } + let Some((x, y)) = mouse_to_grid(window, camera, config, ui_query) else { + return; + }; + + println!("Debug Toggle Click: ({}, {})", x, y); + grid.map_tile_state( + (x, y), + |state| match state { + TileState::Unclaimed => TileState::Empty, + TileState::Empty => TileState::Occupied { + seed: ItemType::BerrySeed { + name: "Debug".into(), + }, + watered: false, + growth_stage: 0, + withered: false, + dry_counter: 0, + }, + TileState::Occupied { .. } => TileState::Unclaimed, + }, + tile_query, + ) + .unwrap_or_else(|_| ()); } } @@ -119,3 +152,14 @@ fn next_phase(mut messages: MessageWriter, keys: Res>, + mut commands: Commands, + game_config: Res, + asset_server: Res, +) { + if keys.just_pressed(KeyCode::KeyP) { + open_shop(&mut commands, &game_config, &asset_server); + } +} diff --git a/src/features/input/utils.rs b/src/features/input/utils.rs new file mode 100644 index 0000000..7f38b6e --- /dev/null +++ b/src/features/input/utils.rs @@ -0,0 +1,28 @@ +use crate::prelude::*; +use bevy::window::PrimaryWindow; + +pub fn mouse_to_grid( + window: Single<&Window, With>, + camera: Single<(&Camera, &GlobalTransform), With>, + config: Res, + ui_query: Query<(&ComputedNode, &GlobalTransform), With>, +) -> Option<(u32, u32)> { + let (cam, cam_transform) = *camera; + + let Some(cursor_pos) = window.cursor_position() else { + return None; + }; + if ui_blocks(window, cursor_pos, ui_query) { + return None; + } + let Ok(world_pos) = cam.viewport_to_world(cam_transform, cursor_pos) else { + return None; + }; + let Ok(grid_pos) = + world_to_grid_coords(world_pos.origin, config.grid_width, config.grid_height) + else { + return None; + }; + + Some(grid_pos) +} diff --git a/src/features/inventory/components.rs b/src/features/inventory/components.rs index bf2e418..b5889bc 100644 --- a/src/features/inventory/components.rs +++ b/src/features/inventory/components.rs @@ -1,30 +1,104 @@ +use crate::features::config::components::BerrySeedConfig; use crate::prelude::*; -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] pub enum ItemType { Berry, + BerrySeed { name: String }, + Shovel, } impl ItemType { - pub fn singular(&self) -> String { + pub fn singular(&self, game_config: &GameConfig) -> String { match self { - ItemType::Berry => "Beere", + 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 + .map(|s| s.name.clone()) + .unwrap_or_else(|| format!("Unbekannter Samen ({})", name)) + } } - .into() } - pub fn plural(&self) -> String { + pub fn plural(&self, game_config: &GameConfig) -> String { match self { - ItemType::Berry => "Beeren", + 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 + .map(|s| s.name.clone()) + .unwrap_or_else(|| format!("Unbekannte Samen ({})", name)) + } } - .into() } - pub fn description(&self) -> String { + pub fn description(&self, game_config: &GameConfig) -> String { match self { - ItemType::Berry => "Von Pflanzen erntbar. Kann im Shop zum Einkaufen benutzt werden.", + 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. Nach {} Fokus-Phasen ausgewachsen. Erhalte beim Ernten {} {}.", + s.growth_stages, + s.grants, + match s.grants { + 1 => ItemType::Berry.singular(game_config), + _ => ItemType::Berry.plural(game_config), + } + ) + } else { + format!("Unbekannter Samen ({})", name) + } + } + } + } + + pub fn get_seed_config<'a>(&self, game_config: &'a GameConfig) -> Option<&'a BerrySeedConfig> { + match self { + 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, + 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 { + AseSlice { + name: s.slice.clone(), + aseprite: asset_server.load("seed.aseprite"), + } + } else { + // Fallback for unknown seed + AseSlice { + name: "Seed1".into(), + aseprite: asset_server.load("seed.aseprite"), + } + } + } } - .into() } } @@ -39,6 +113,84 @@ pub struct Inventory { pub items: Vec, } +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( + &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 = None; + let mut current_stack_amount: u32 = 0; + let mut entity_id_to_update: Option = 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, diff --git a/src/features/inventory/mod.rs b/src/features/inventory/mod.rs index 1b60ee8..2218ca2 100644 --- a/src/features/inventory/mod.rs +++ b/src/features/inventory/mod.rs @@ -11,6 +11,9 @@ impl Plugin for InventoryPlugin { app.init_resource::(); app.add_systems(Update, buttons.run_if(in_state(AppState::GameScreen))); + + #[cfg(debug_assertions)] + app.add_systems(Update, debug_modify_berries); } } @@ -19,12 +22,14 @@ fn buttons( mut interaction_query: Query<(&Interaction, &ButtonType), (Changed, With