From e8af0add0bbadc254b771e907a9a55e64ff9600f Mon Sep 17 00:00:00 2001 From: demenik Date: Thu, 11 Dec 2025 19:52:53 +0100 Subject: [PATCH 1/7] feat: Add achievement sprite (#47) --- assets/achievement.aseprite | Bin 0 -> 494 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/achievement.aseprite diff --git a/assets/achievement.aseprite b/assets/achievement.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..2d1c4137345f0b510065f23bad693e05f994a066 GIT binary patch literal 494 zcmaFI$iVPmDIsQYs@+_W_U`bk-=EB8J`Kp)XJ9@h&pT5;$Xmt8 z#?s!t#2}$5I`3if?Emp+pPat^=l}ozv)68KscX;7%8CpNDs$>uV`OwQJo$R{uKwv> zUHJz8&l$;sEK^`cvi?7i&CcMHSeaU+U}y|>0V^b5rw5khB2<4HR$; z417SWpuhx{`v1QgNV6(10KLP=@E;-w6yXEHiaE&%4bA^ul1e`O|E|9I{|Sa8?TG>o zm)Xr<|F={Y*{boYq3A`~;WKJW_>Z-{Y~I+pkv~>m@|)p=hxR=NtS6HXTU=TE!nMp= e&U(YEqmRU+ULN+)c6p&{9@F7>nc=U8K?4A(f^$Xy literal 0 HcmV?d00001 From d58b23c1b1261243c34751275111e1a24ea9f3c1 Mon Sep 17 00:00:00 2001 From: demenik Date: Thu, 11 Dec 2025 20:19:00 +0100 Subject: [PATCH 2/7] feat: Add title and description methods to AchievementId (#47) --- src/features/achievement/components.rs | 40 +++++++++++++++++++------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/features/achievement/components.rs b/src/features/achievement/components.rs index 788daf8..fef5673 100644 --- a/src/features/achievement/components.rs +++ b/src/features/achievement/components.rs @@ -19,21 +19,41 @@ pub enum AchievementId { } impl AchievementId { - /// Label to be displayed ingame - pub fn label(&self) -> String { + /// Title to be displayed ingame + pub fn title(&self) -> String { match self { - AchievementId::FirstSteps => "Erste Schritte: Verdiene eine Beere.", - AchievementId::MasterFarmer => "Meisterbauer: Verdiene 100 Beeren.", - AchievementId::BerryTycoon => "Beeren-Tycoon: Verdiene 1.000 Beeren.", - AchievementId::GettingStarted => "Aller Anfang: Schließe deine erste Fokus-Phase ab.", - AchievementId::FocusMaster => "Fokus-Meister: Schließe 10 Fokus-Phasen ab.", - AchievementId::ZenMaster => "Zen-Meister: Schließe 50 Fokus-Phasen ab.", - AchievementId::Negligent => "Nachlässig: Lasse eine Pflanze verdorren.", - AchievementId::CompostKing => "Kompost-König: Lasse 10 Pflanzen verdorren.", + AchievementId::FirstSteps => "Erste Schritte", + AchievementId::MasterFarmer => "Meisterbauer", + AchievementId::BerryTycoon => "Beeren-Tycoon", + AchievementId::GettingStarted => "Aller Anfang", + AchievementId::FocusMaster => "Fokus-Meister", + AchievementId::ZenMaster => "Zen-Meister", + AchievementId::Negligent => "Nachlässig", + AchievementId::CompostKing => "Kompost-König", } .into() } + /// Description to be displayed ingame + pub fn description(&self) -> String { + match self { + AchievementId::FirstSteps => "Verdiene eine Beere.", + AchievementId::MasterFarmer => "Verdiene 100 Beeren.", + AchievementId::BerryTycoon => "Verdiene 1.000 Beeren.", + AchievementId::GettingStarted => "Schließe deine erste Fokus-Phase ab.", + AchievementId::FocusMaster => "Schließe 10 Fokus-Phasen ab.", + AchievementId::ZenMaster => "Schließe 50 Fokus-Phasen ab.", + AchievementId::Negligent => "Lasse eine Pflanze verdorren.", + AchievementId::CompostKing => "Lasse 10 Pflanzen verdorren.", + } + .into() + } + + /// Label to be displayed ingame (Title: Description) + pub fn label(&self) -> String { + format!("{}: {}", self.title(), self.description()) + } + /// Checks if an achievement's conditions are met pub fn conditions_met(&self, tracker: &SessionTracker) -> bool { match self { From 8c9a27f0df6dd1f1812b963e0673b6028d6e5010 Mon Sep 17 00:00:00 2001 From: demenik Date: Thu, 11 Dec 2025 20:20:30 +0100 Subject: [PATCH 3/7] feat: Implement achievement menu UI (#47) --- src/features/achievement/mod.rs | 1 + src/features/achievement/ui.rs | 90 +++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/features/achievement/ui.rs diff --git a/src/features/achievement/mod.rs b/src/features/achievement/mod.rs index df4d7a5..3d8d7a0 100644 --- a/src/features/achievement/mod.rs +++ b/src/features/achievement/mod.rs @@ -5,6 +5,7 @@ use components::{AchievementId, AchievementProgress}; use strum::IntoEnumIterator; pub mod components; +pub mod ui; pub struct AchievementPlugin; diff --git a/src/features/achievement/ui.rs b/src/features/achievement/ui.rs new file mode 100644 index 0000000..8dbe75c --- /dev/null +++ b/src/features/achievement/ui.rs @@ -0,0 +1,90 @@ +use crate::features::achievement::components::{AchievementId, AchievementProgress}; +use crate::features::ui::ui::popups::spawn_popup; +use crate::prelude::*; +use strum::IntoEnumIterator; + +#[derive(Component)] +pub enum AchievementRootMarker { + Menu, +} + +pub fn open_achievements_menu(commands: &mut Commands, progress: &AchievementProgress) { + spawn_popup( + commands, + AchievementRootMarker::Menu, + "Erfolge", + Node { + width: px(700), + height: px(500), + ..default() + }, + |parent| { + // Scrollable Content + parent + .spawn((Node { + width: percent(100), + height: percent(100), + flex_direction: FlexDirection::Column, + overflow: Overflow::scroll_y(), + padding: UiRect::all(px(10)), + row_gap: px(10), + ..default() + },)) + .with_children(|list| { + for id in AchievementId::iter() { + let unlocked = progress.is_unlocked(&id); + let color = if unlocked { + Color::WHITE + } else { + Color::srgb(0.5, 0.5, 0.5) + }; + + let bg_color = if unlocked { + Color::srgb(0.2, 0.3, 0.2) // Dark Greenish for unlocked + } else { + Color::srgb(0.1, 0.1, 0.1) // Dark Grey for locked + }; + + list.spawn(( + Node { + width: percent(100), + padding: UiRect::all(px(10)), + flex_direction: FlexDirection::Column, + row_gap: px(5), + border: UiRect::all(px(2)), + ..default() + }, + BackgroundColor(bg_color), + BorderColor::all(if unlocked { + Color::srgb(0.4, 0.8, 0.4) + } else { + Color::BLACK + }), + BorderRadius::all(px(5)), + )) + .with_children(|item| { + // Title + item.spawn(text(id.title(), 20.0, color)); + // Description + item.spawn(text(id.description(), 16.0, color)); + // Status Text + let status = if unlocked { + "Freigeschaltet" + } else { + "Gesperrt" + }; + item.spawn(text( + status, + 12.0, + if unlocked { + Color::srgb(0.6, 1.0, 0.6) + } else { + Color::srgb(0.7, 0.3, 0.3) + }, + )); + }); + } + }); + }, + ); +} From 52dd300ca0bfb2e9d6ac81b3fc50512142089dc2 Mon Sep 17 00:00:00 2001 From: demenik Date: Thu, 11 Dec 2025 20:21:02 +0100 Subject: [PATCH 4/7] feat: Add achievements button to HUD settings (#47) --- src/features/hud/components.rs | 1 + src/features/hud/mod.rs | 11 +++++++++++ src/features/hud/ui/settings.rs | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/src/features/hud/components.rs b/src/features/hud/components.rs index ccd9a63..449cb5a 100644 --- a/src/features/hud/components.rs +++ b/src/features/hud/components.rs @@ -21,6 +21,7 @@ pub enum ButtonType { SettingsOpen, SettingsExit, SettingsSave, + SettingsAchievements, SettingsTimerChange { input: SettingsTimerInput, amount: i32, diff --git a/src/features/hud/mod.rs b/src/features/hud/mod.rs index f736d89..27fa6fa 100644 --- a/src/features/hud/mod.rs +++ b/src/features/hud/mod.rs @@ -1,3 +1,4 @@ +use crate::features::achievement::{components::AchievementProgress, ui::open_achievements_menu}; use crate::features::phase::components::TimerSettings; use crate::features::savegame::messages::SavegameDumpMessage; use crate::features::{inventory, shop}; @@ -151,6 +152,8 @@ fn buttons( mut next_state: ResMut>, mut timer_settings: ResMut, keys: Res>, + achievement_progress: Res, + root_query: Query<(Entity, &RootMarker)>, ) { let shift_multiplier = if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) { 10 @@ -171,6 +174,14 @@ fn buttons( ButtonType::SettingsSave => { savegame_messages.write(SavegameDumpMessage); } + ButtonType::SettingsAchievements => { + open_achievements_menu(&mut commands, &achievement_progress); + for (entity, root) in root_query.iter() { + if let RootMarker::Settings = root { + commands.entity(entity).despawn(); + } + } + } ButtonType::SettingsTimerChange { input, amount } => match input { SettingsTimerInput::Minutes(timer_type) => { timer_settings.change(timer_type, 60 * amount * shift_multiplier) diff --git a/src/features/hud/ui/settings.rs b/src/features/hud/ui/settings.rs index 1ae8962..0d1b70d 100644 --- a/src/features/hud/ui/settings.rs +++ b/src/features/hud/ui/settings.rs @@ -27,6 +27,12 @@ pub fn open_settings(commands: &mut Commands) { ButtonVariant::Secondary, Node::from_padding(UiRect::all(px(10))), |color| text("Spiel speichern", 24.0, color) + ), + button( + ButtonType::SettingsAchievements, + ButtonVariant::Secondary, + Node::from_padding(UiRect::all(px(10))), + |color| text("Erfolge", 24.0, color) ),( Node { justify_content: JustifyContent::Center, From 2857f59a853e8fa6a53e8ea3fc7cc98a533510fe Mon Sep 17 00:00:00 2001 From: demenik Date: Thu, 11 Dec 2025 20:21:46 +0100 Subject: [PATCH 5/7] feat: Show achievement progress in savegame load menu (#47) --- src/features/savegame/components.rs | 7 ++++++- src/features/savegame/ui/load.rs | 25 ++++++++++++++++++++++++- src/features/start_screen/mod.rs | 3 ++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/features/savegame/components.rs b/src/features/savegame/components.rs index 4ed2aaf..349bed5 100644 --- a/src/features/savegame/components.rs +++ b/src/features/savegame/components.rs @@ -1,3 +1,4 @@ +use crate::features::achievement::components::AchievementProgress; use crate::prelude::*; use std::fs; use std::path::PathBuf; @@ -7,18 +8,20 @@ use std::path::PathBuf; pub struct SavegamePath(pub PathBuf); /// Metadata about a savegame. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SavegameInfo { pub path: SavegamePath, pub index: u32, pub total_berries: u32, pub completed_focus: u32, + pub achievement_progress: AchievementProgress, } /// Helper for partial JSON deserialization. #[derive(Deserialize)] struct PartialSaveData { session_tracker: PartialSessionTracker, + achievement_progress: AchievementProgress, } /// Helper for partial JSON deserialization of session stats. @@ -87,6 +90,7 @@ impl SavegamePath { index, total_berries: data.session_tracker.total_berries_earned, completed_focus: data.session_tracker.completed_focus_phases, + achievement_progress: data.achievement_progress, }); } @@ -113,4 +117,5 @@ pub enum RootMarker { pub enum ButtonType { SavegameLoad { savegame_path: SavegamePath }, SavegameDelete { savegame_path: SavegamePath }, + Achievements { savegame: SavegameInfo }, } diff --git a/src/features/savegame/ui/load.rs b/src/features/savegame/ui/load.rs index 8871061..a03eb7c 100644 --- a/src/features/savegame/ui/load.rs +++ b/src/features/savegame/ui/load.rs @@ -1,8 +1,9 @@ use super::super::components::{ButtonType, RootMarker}; +use crate::features::achievement::ui::open_achievements_menu; use crate::{features::savegame::messages::SavegameLoadMessage, prelude::*}; /// Spawns the "Load Game" popup. -pub fn spawn_load_popup(commands: &mut Commands) { +pub fn spawn_load_popup(commands: &mut Commands, asset_server: &AssetServer) { spawn_popup( commands, RootMarker::PopupSavegameLoad, @@ -78,6 +79,25 @@ pub fn spawn_load_popup(commands: &mut Commands) { ..default() }, |color| text("X", 24.0, color) + ), + button( + ButtonType::Achievements { + savegame: savegame.clone() + }, + ButtonVariant::Primary, + Node { + width: px(40), + height: px(40), + ..default() + }, + |_| ( + ImageNode::default(), + AseSlice { + aseprite: asset_server + .load("achievement.aseprite"), + name: "Achievement".into() + } + ) ) ], ) @@ -111,6 +131,9 @@ pub fn load_popup_handler( println!("Error while deleting savegame: {:?}", e); } } + ButtonType::Achievements { savegame } => { + open_achievements_menu(&mut commands, &savegame.achievement_progress); + } }; for (entity, root) in root_query.iter() { diff --git a/src/features/start_screen/mod.rs b/src/features/start_screen/mod.rs index 81a833d..3109e67 100644 --- a/src/features/start_screen/mod.rs +++ b/src/features/start_screen/mod.rs @@ -80,12 +80,13 @@ fn menu( mut commands: Commands, mut next_state: ResMut>, mut interaction_query: Query<(&Interaction, &ButtonType), (Changed, With