feat: Add achievement tracking and persistence (#42)
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -210,7 +210,7 @@ dependencies = [
|
|||||||
"flate2",
|
"flate2",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"nom",
|
"nom",
|
||||||
"strum",
|
"strum 0.26.3",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3940,6 +3940,8 @@ dependencies = [
|
|||||||
"directories",
|
"directories",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"strum 0.27.2",
|
||||||
|
"strum_macros 0.27.2",
|
||||||
"tungstenite",
|
"tungstenite",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -4565,9 +4567,15 @@ version = "0.26.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"strum_macros",
|
"strum_macros 0.26.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.27.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum_macros"
|
name = "strum_macros"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
@@ -4581,6 +4589,18 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.27.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "svg_fmt"
|
name = "svg_fmt"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
@@ -4714,7 +4734,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.2",
|
"rustix 1.1.2",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ serde_json = "1.0"
|
|||||||
directories = "6.0"
|
directories = "6.0"
|
||||||
tungstenite = { version = "0.28.0", features = ["native-tls"] }
|
tungstenite = { version = "0.28.0", features = ["native-tls"] }
|
||||||
url = "2.5.7"
|
url = "2.5.7"
|
||||||
|
strum = "0.27.2"
|
||||||
|
strum_macros = "0.27.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
uuid = "1.18.1"
|
uuid = "1.18.1"
|
||||||
|
|||||||
65
src/features/achievement/components.rs
Normal file
65
src/features/achievement/components.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use crate::{features::phase::components::SessionTracker, prelude::*};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use strum_macros::EnumIter;
|
||||||
|
|
||||||
|
/// Represents an unlockable achievement.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, EnumIter)]
|
||||||
|
pub enum AchievementId {
|
||||||
|
// Berry achievements
|
||||||
|
FirstSteps,
|
||||||
|
MasterFarmer,
|
||||||
|
BerryTycoon,
|
||||||
|
// Focus achievements,
|
||||||
|
GettingStarted,
|
||||||
|
FocusMaster,
|
||||||
|
ZenMaster,
|
||||||
|
// Withered achievements
|
||||||
|
Negligent,
|
||||||
|
CompostKing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AchievementId {
|
||||||
|
/// Label to be displayed ingame
|
||||||
|
pub fn label(&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.",
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an achievement's conditions are met
|
||||||
|
pub fn conditions_met(&self, tracker: &SessionTracker) -> bool {
|
||||||
|
match self {
|
||||||
|
AchievementId::FirstSteps => tracker.total_berries_earned >= 1,
|
||||||
|
AchievementId::MasterFarmer => tracker.total_berries_earned >= 100,
|
||||||
|
AchievementId::BerryTycoon => tracker.total_berries_earned >= 1000,
|
||||||
|
AchievementId::GettingStarted => tracker.completed_focus_phases >= 1,
|
||||||
|
AchievementId::FocusMaster => tracker.completed_focus_phases >= 10,
|
||||||
|
AchievementId::ZenMaster => tracker.completed_focus_phases >= 50,
|
||||||
|
AchievementId::Negligent => tracker.total_plants_withered >= 1,
|
||||||
|
AchievementId::CompostKing => tracker.total_plants_withered >= 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Default, Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct AchievementProgress {
|
||||||
|
pub unlocked: HashMap<AchievementId, bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AchievementProgress {
|
||||||
|
pub fn is_unlocked(&self, id: &AchievementId) -> bool {
|
||||||
|
*self.unlocked.get(id).unwrap_or(&false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(&mut self, id: AchievementId) {
|
||||||
|
self.unlocked.insert(id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/features/achievement/mod.rs
Normal file
32
src/features/achievement/mod.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use crate::features::notification::components::Notifications;
|
||||||
|
use crate::features::phase::components::SessionTracker;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use components::{AchievementId, AchievementProgress};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
pub mod components;
|
||||||
|
|
||||||
|
pub struct AchievementPlugin;
|
||||||
|
|
||||||
|
impl Plugin for AchievementPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<AchievementProgress>();
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
check_achievements.run_if(in_state(AppState::GameScreen)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_achievements(
|
||||||
|
tracker: Res<SessionTracker>,
|
||||||
|
mut progress: ResMut<AchievementProgress>,
|
||||||
|
mut notifications: ResMut<Notifications>,
|
||||||
|
) {
|
||||||
|
for achievement in AchievementId::iter() {
|
||||||
|
if !progress.is_unlocked(&achievement) && achievement.conditions_met(&tracker) {
|
||||||
|
progress.unlock(achievement.clone());
|
||||||
|
notifications.info(Some("Erfolg freigeschaltet!"), achievement.label());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod achievement;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod game_screen;
|
pub mod game_screen;
|
||||||
@@ -14,6 +15,7 @@ pub mod start_screen;
|
|||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod wonderevent;
|
pub mod wonderevent;
|
||||||
|
|
||||||
|
pub use achievement::AchievementPlugin;
|
||||||
pub use core::CorePlugin;
|
pub use core::CorePlugin;
|
||||||
pub use game_screen::GameScreenPlugin;
|
pub use game_screen::GameScreenPlugin;
|
||||||
pub use grid::GridPlugin;
|
pub use grid::GridPlugin;
|
||||||
|
|||||||
@@ -67,4 +67,5 @@ impl Default for TimerSettings {
|
|||||||
pub struct SessionTracker {
|
pub struct SessionTracker {
|
||||||
pub completed_focus_phases: u32,
|
pub completed_focus_phases: u32,
|
||||||
pub total_berries_earned: u32,
|
pub total_berries_earned: u32,
|
||||||
|
pub total_plants_withered: u32,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ pub fn next_phase(
|
|||||||
pub fn handle_continue(
|
pub fn handle_continue(
|
||||||
mut messages: MessageReader<NextPhaseMessage>,
|
mut messages: MessageReader<NextPhaseMessage>,
|
||||||
mut phase_res: ResMut<CurrentPhase>,
|
mut phase_res: ResMut<CurrentPhase>,
|
||||||
session_tracker: Res<SessionTracker>,
|
mut session_tracker: ResMut<SessionTracker>,
|
||||||
settings: Res<TimerSettings>,
|
settings: Res<TimerSettings>,
|
||||||
mut tile_query: Query<&mut TileState>,
|
mut tile_query: Query<&mut TileState>,
|
||||||
game_config: Res<GameConfig>,
|
game_config: Res<GameConfig>,
|
||||||
@@ -256,6 +256,10 @@ pub fn handle_continue(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if new_withered && !*withered {
|
||||||
|
session_tracker.total_plants_withered += 1;
|
||||||
|
}
|
||||||
|
|
||||||
*state = TileState::Occupied {
|
*state = TileState::Occupied {
|
||||||
seed: seed.clone(),
|
seed: seed.clone(),
|
||||||
watered: false,
|
watered: false,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::features::achievement::components::AchievementProgress;
|
||||||
use crate::features::phase::components::{SessionTracker, TimerSettings};
|
use crate::features::phase::components::{SessionTracker, TimerSettings};
|
||||||
use crate::features::savegame::ui::load_popup_handler;
|
use crate::features::savegame::ui::load_popup_handler;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -34,6 +35,7 @@ struct SaveData {
|
|||||||
tiles: Vec<Vec<TileState>>,
|
tiles: Vec<Vec<TileState>>,
|
||||||
current_phase: CurrentPhase,
|
current_phase: CurrentPhase,
|
||||||
session_tracker: SessionTracker,
|
session_tracker: SessionTracker,
|
||||||
|
achievement_progress: AchievementProgress,
|
||||||
timer_settings: TimerSettings,
|
timer_settings: TimerSettings,
|
||||||
pom_position: GridPosition,
|
pom_position: GridPosition,
|
||||||
inventory: Vec<ItemStack>,
|
inventory: Vec<ItemStack>,
|
||||||
@@ -47,6 +49,7 @@ fn dump_savegame(
|
|||||||
tile_query: Query<&TileState>,
|
tile_query: Query<&TileState>,
|
||||||
phase: Res<CurrentPhase>,
|
phase: Res<CurrentPhase>,
|
||||||
tracker: Res<SessionTracker>,
|
tracker: Res<SessionTracker>,
|
||||||
|
achievement_progress: Res<AchievementProgress>,
|
||||||
settings: Res<TimerSettings>,
|
settings: Res<TimerSettings>,
|
||||||
pom_query: Query<&GridPosition, With<Pom>>,
|
pom_query: Query<&GridPosition, With<Pom>>,
|
||||||
inventory: Res<Inventory>,
|
inventory: Res<Inventory>,
|
||||||
@@ -85,6 +88,7 @@ fn dump_savegame(
|
|||||||
tiles: tile_states,
|
tiles: tile_states,
|
||||||
current_phase: phase.clone(),
|
current_phase: phase.clone(),
|
||||||
session_tracker: tracker.clone(),
|
session_tracker: tracker.clone(),
|
||||||
|
achievement_progress: achievement_progress.clone(),
|
||||||
timer_settings: settings.clone(),
|
timer_settings: settings.clone(),
|
||||||
pom_position: *pom_pos,
|
pom_position: *pom_pos,
|
||||||
inventory: item_stacks,
|
inventory: item_stacks,
|
||||||
@@ -118,6 +122,7 @@ fn load_savegame(
|
|||||||
mut tile_query: Query<&mut TileState>,
|
mut tile_query: Query<&mut TileState>,
|
||||||
mut phase: ResMut<CurrentPhase>,
|
mut phase: ResMut<CurrentPhase>,
|
||||||
mut tracker: ResMut<SessionTracker>,
|
mut tracker: ResMut<SessionTracker>,
|
||||||
|
mut achievement_progress: ResMut<AchievementProgress>,
|
||||||
mut settings: ResMut<TimerSettings>,
|
mut settings: ResMut<TimerSettings>,
|
||||||
mut pom_query: Query<(&mut GridPosition, &mut Transform), With<Pom>>,
|
mut pom_query: Query<(&mut GridPosition, &mut Transform), With<Pom>>,
|
||||||
mut inventory: ResMut<Inventory>,
|
mut inventory: ResMut<Inventory>,
|
||||||
@@ -134,6 +139,7 @@ fn load_savegame(
|
|||||||
Ok(save_data) => {
|
Ok(save_data) => {
|
||||||
*phase = save_data.current_phase;
|
*phase = save_data.current_phase;
|
||||||
*tracker = save_data.session_tracker;
|
*tracker = save_data.session_tracker;
|
||||||
|
*achievement_progress = save_data.achievement_progress;
|
||||||
*settings = save_data.timer_settings;
|
*settings = save_data.timer_settings;
|
||||||
|
|
||||||
if let Ok((mut pom_pos, mut pom_transform)) = pom_query.single_mut() {
|
if let Ok((mut pom_pos, mut pom_transform)) = pom_query.single_mut() {
|
||||||
@@ -185,11 +191,13 @@ fn reset_savegame(
|
|||||||
mut tile_query: Query<&mut TileState>,
|
mut tile_query: Query<&mut TileState>,
|
||||||
mut phase: ResMut<CurrentPhase>,
|
mut phase: ResMut<CurrentPhase>,
|
||||||
mut tracker: ResMut<SessionTracker>,
|
mut tracker: ResMut<SessionTracker>,
|
||||||
|
mut achievement_progress: ResMut<AchievementProgress>,
|
||||||
mut settings: ResMut<TimerSettings>,
|
mut settings: ResMut<TimerSettings>,
|
||||||
mut pom_query: Query<(&mut GridPosition, &mut Transform), With<Pom>>,
|
mut pom_query: Query<(&mut GridPosition, &mut Transform), With<Pom>>,
|
||||||
mut inventory: ResMut<Inventory>,
|
mut inventory: ResMut<Inventory>,
|
||||||
) {
|
) {
|
||||||
*tracker = SessionTracker::default();
|
*tracker = SessionTracker::default();
|
||||||
|
*achievement_progress = AchievementProgress::default();
|
||||||
*settings = TimerSettings::default();
|
*settings = TimerSettings::default();
|
||||||
*phase = CurrentPhase(Phase::Focus {
|
*phase = CurrentPhase(Phase::Focus {
|
||||||
duration: settings.focus_duration as f32,
|
duration: settings.focus_duration as f32,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ fn main() {
|
|||||||
features::ShopPlugin,
|
features::ShopPlugin,
|
||||||
features::WonderEventPlugin,
|
features::WonderEventPlugin,
|
||||||
features::NotificationPlugin,
|
features::NotificationPlugin,
|
||||||
|
features::AchievementPlugin,
|
||||||
))
|
))
|
||||||
.insert_resource(config)
|
.insert_resource(config)
|
||||||
.add_systems(Startup, overwrite_default_font)
|
.add_systems(Startup, overwrite_default_font)
|
||||||
|
|||||||
Reference in New Issue
Block a user