use chrono::{DateTime, Datelike, Timelike, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use tauri_plugin_store::StoreExt; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum AchievementId { // Token Milestones FirstSteps, // 1,000 tokens GrowingStrong, // 10,000 tokens BlossomingCoder, // 100,000 tokens TokenMaster, // 1,000,000 tokens // Code Generation HelloWorld, // First code block CodeWizard, // 100 code blocks ThousandBlocks, // 1,000 code blocks // File Operations FileManipulator, // 10 files edited FileArchitect, // 100 files edited // Conversation milestones ConversationStarter, // 10 messages ChattyKathy, // 100 messages Conversationalist, // 1,000 messages // Tool usage Toolsmith, // 5 different tools ToolMaster, // 10 different tools // Time-based achievements EarlyBird, // Started session 5-7 AM NightOwl, // Coding after midnight AllNighter, // Worked 2-5 AM WeekendWarrior, // Coding on weekend DedicatedDeveloper, // 30 days in a row // Search and exploration Explorer, // 50 searches MasterSearcher, // 500 searches // Session achievements QuickSession, // Productive session < 5 min FocusedWork, // 30 min session DeepDive, // 2 hour session MarathonSession, // 5+ hour session // Special achievements FirstMessage, // First message sent FirstTool, // First tool used FirstCodeBlock, // First code generated FirstFileEdit, // First file edit Polyglot, // 5+ languages in one session SpeedCoder, // 10 code blocks in 10 minutes ClaudeConnoisseur, // Used all Claude models MarathonCoder, // 10k tokens in one session // Relationship & Greetings GoodMorning, // Say "good morning" GoodNight, // Say "good night" or "goodnight" ThankYou, // Say "thank you" or "thanks" LoveYou, // Say "love you" or "ily" // Personality & Fun EmojiUser, // Use an emoji in a message QuestionMaster, // Use "?" in 20 messages CapsLock, // Send a message in ALL CAPS PleaseAndThankYou, // Use "please" in messages // Git & Development GitGuru, // Use git commands 10 times TestWriter, // Create test files Debugger, // Fix bugs (messages with "fix", "bug", "error") // Tool Mastery BashMaster, // Use Bash tool 50 times FileExplorer, // Use Read tool 100 times SearchExpert, // Use Grep tool 50 times } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Achievement { pub id: AchievementId, pub name: String, pub description: String, pub icon: String, pub unlocked_at: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AchievementProgress { pub unlocked: HashSet, pub newly_unlocked: Vec, // Achievements unlocked but not yet notified #[serde(skip)] pub session_start: Option>, } impl AchievementProgress { pub fn new() -> Self { Self { unlocked: HashSet::new(), newly_unlocked: Vec::new(), session_start: None, } } pub fn unlock(&mut self, achievement: AchievementId) -> bool { if self.unlocked.insert(achievement.clone()) { self.newly_unlocked.push(achievement); true } else { false } } #[cfg(test)] pub fn take_newly_unlocked(&mut self) -> Vec { std::mem::take(&mut self.newly_unlocked) } #[cfg(test)] pub fn is_unlocked(&self, achievement: &AchievementId) -> bool { self.unlocked.contains(achievement) } pub fn start_session(&mut self) { self.session_start = Some(Utc::now()); } } impl Default for AchievementProgress { fn default() -> Self { Self::new() } } pub fn get_achievement_info(id: &AchievementId) -> Achievement { match id { // Token Milestones AchievementId::FirstSteps => Achievement { id: id.clone(), name: "First Steps!".to_string(), description: "Used 1,000 tokens".to_string(), icon: "๐ŸŒฑ".to_string(), unlocked_at: None, }, AchievementId::GrowingStrong => Achievement { id: id.clone(), name: "Growing Strong!".to_string(), description: "Used 10,000 tokens".to_string(), icon: "๐ŸŒธ".to_string(), unlocked_at: None, }, AchievementId::BlossomingCoder => Achievement { id: id.clone(), name: "Blossoming Coder!".to_string(), description: "Used 100,000 tokens".to_string(), icon: "๐ŸŒบ".to_string(), unlocked_at: None, }, AchievementId::TokenMaster => Achievement { id: id.clone(), name: "Token Master!".to_string(), description: "Used 1,000,000 tokens".to_string(), icon: "๐ŸŒŸ".to_string(), unlocked_at: None, }, // Code Generation AchievementId::HelloWorld => Achievement { id: id.clone(), name: "Hello World!".to_string(), description: "Generated your first code block".to_string(), icon: "๐Ÿ“".to_string(), unlocked_at: None, }, AchievementId::CodeWizard => Achievement { id: id.clone(), name: "Code Wizard!".to_string(), description: "Generated 100 code blocks".to_string(), icon: "๐ŸŽฏ".to_string(), unlocked_at: None, }, AchievementId::ThousandBlocks => Achievement { id: id.clone(), name: "Thousand Blocks".to_string(), description: "1,000 code blocks! You're a code machine!".to_string(), icon: "๐Ÿ—๏ธ".to_string(), unlocked_at: None, }, // File Operations AchievementId::FileManipulator => Achievement { id: id.clone(), name: "File Manipulator".to_string(), description: "Edited 10 files".to_string(), icon: "๐Ÿ“".to_string(), unlocked_at: None, }, AchievementId::FileArchitect => Achievement { id: id.clone(), name: "File Architect".to_string(), description: "Created or edited 100 files".to_string(), icon: "๐Ÿ›๏ธ".to_string(), unlocked_at: None, }, // Conversation milestones AchievementId::ConversationStarter => Achievement { id: id.clone(), name: "Conversation Starter".to_string(), description: "Exchanged 10 messages".to_string(), icon: "๐Ÿ’ฌ".to_string(), unlocked_at: None, }, AchievementId::ChattyKathy => Achievement { id: id.clone(), name: "Chatty Kathy".to_string(), description: "100 messages exchanged".to_string(), icon: "๐Ÿ—ฃ๏ธ".to_string(), unlocked_at: None, }, AchievementId::Conversationalist => Achievement { id: id.clone(), name: "Master Conversationalist".to_string(), description: "1,000 messages! We're really connecting!".to_string(), icon: "๐Ÿ’–".to_string(), unlocked_at: None, }, // Tool usage AchievementId::Toolsmith => Achievement { id: id.clone(), name: "Toolsmith".to_string(), description: "Used 5 different tools".to_string(), icon: "๐Ÿ”จ".to_string(), unlocked_at: None, }, AchievementId::ToolMaster => Achievement { id: id.clone(), name: "Tool Master".to_string(), description: "Used 10 different tools efficiently".to_string(), icon: "๐Ÿ› ๏ธ".to_string(), unlocked_at: None, }, // Time-based achievements AchievementId::EarlyBird => Achievement { id: id.clone(), name: "Early Bird".to_string(), description: "Started a session between 5 AM and 7 AM".to_string(), icon: "๐ŸŒ…".to_string(), unlocked_at: None, }, AchievementId::NightOwl => Achievement { id: id.clone(), name: "Night Owl".to_string(), description: "Coding after midnight".to_string(), icon: "๐Ÿฆ‰".to_string(), unlocked_at: None, }, AchievementId::AllNighter => Achievement { id: id.clone(), name: "All Nighter".to_string(), description: "Worked through the night (2 AM - 5 AM)".to_string(), icon: "๐ŸŒ™".to_string(), unlocked_at: None, }, AchievementId::WeekendWarrior => Achievement { id: id.clone(), name: "Weekend Warrior".to_string(), description: "Coding on a weekend".to_string(), icon: "โš”๏ธ".to_string(), unlocked_at: None, }, AchievementId::DedicatedDeveloper => Achievement { id: id.clone(), name: "Dedicated Developer".to_string(), description: "Coded for 30 days in a row".to_string(), icon: "๐Ÿ†".to_string(), unlocked_at: None, }, // Search and exploration AchievementId::Explorer => Achievement { id: id.clone(), name: "Explorer".to_string(), description: "Used search tools 50 times".to_string(), icon: "๐Ÿ”".to_string(), unlocked_at: None, }, AchievementId::MasterSearcher => Achievement { id: id.clone(), name: "Master Searcher".to_string(), description: "Searched 500 times across files".to_string(), icon: "๐Ÿ•ต๏ธโ€โ™€๏ธ".to_string(), unlocked_at: None, }, // Session achievements AchievementId::QuickSession => Achievement { id: id.clone(), name: "Quick Session".to_string(), description: "Completed a productive session in under 5 minutes".to_string(), icon: "โšก".to_string(), unlocked_at: None, }, AchievementId::FocusedWork => Achievement { id: id.clone(), name: "Focused Work".to_string(), description: "Worked for 30 minutes straight".to_string(), icon: "๐ŸŽฏ".to_string(), unlocked_at: None, }, AchievementId::DeepDive => Achievement { id: id.clone(), name: "Deep Dive".to_string(), description: "Worked for 2 hours continuously".to_string(), icon: "๐ŸŠโ€โ™€๏ธ".to_string(), unlocked_at: None, }, AchievementId::MarathonSession => Achievement { id: id.clone(), name: "Marathon Session".to_string(), description: "5+ hour coding session!".to_string(), icon: "๐Ÿƒโ€โ™€๏ธ".to_string(), unlocked_at: None, }, // Special achievements AchievementId::FirstMessage => Achievement { id: id.clone(), name: "First Message".to_string(), description: "Sent your first message to Hikari".to_string(), icon: "โœจ".to_string(), unlocked_at: None, }, AchievementId::FirstTool => Achievement { id: id.clone(), name: "First Tool".to_string(), description: "Used your first tool".to_string(), icon: "๐Ÿ”ง".to_string(), unlocked_at: None, }, AchievementId::FirstCodeBlock => Achievement { id: id.clone(), name: "First Code".to_string(), description: "Generated your first code block".to_string(), icon: "๐Ÿ“ฆ".to_string(), unlocked_at: None, }, AchievementId::FirstFileEdit => Achievement { id: id.clone(), name: "First Edit".to_string(), description: "Made your first file edit".to_string(), icon: "โœ๏ธ".to_string(), unlocked_at: None, }, AchievementId::Polyglot => Achievement { id: id.clone(), name: "Polyglot".to_string(), description: "Generated code in 5+ languages in one session".to_string(), icon: "๐ŸŒ".to_string(), unlocked_at: None, }, AchievementId::SpeedCoder => Achievement { id: id.clone(), name: "Speed Coder".to_string(), description: "Generated 10 code blocks in 10 minutes".to_string(), icon: "๐Ÿš€".to_string(), unlocked_at: None, }, AchievementId::ClaudeConnoisseur => Achievement { id: id.clone(), name: "Claude Connoisseur".to_string(), description: "Used all available Claude models".to_string(), icon: "๐ŸŽจ".to_string(), unlocked_at: None, }, AchievementId::MarathonCoder => Achievement { id: id.clone(), name: "Marathon Coder".to_string(), description: "10,000 tokens in a single session".to_string(), icon: "๐Ÿƒโ€โ™‚๏ธ".to_string(), unlocked_at: None, }, // Relationship & Greetings AchievementId::GoodMorning => Achievement { id: id.clone(), name: "Good Morning!".to_string(), description: "Greeted Hikari with a good morning".to_string(), icon: "๐ŸŒ…".to_string(), unlocked_at: None, }, AchievementId::GoodNight => Achievement { id: id.clone(), name: "Good Night".to_string(), description: "Said good night to Hikari".to_string(), icon: "๐ŸŒ™".to_string(), unlocked_at: None, }, AchievementId::ThankYou => Achievement { id: id.clone(), name: "Grateful Heart".to_string(), description: "Thanked Hikari for her help".to_string(), icon: "๐Ÿ’".to_string(), unlocked_at: None, }, AchievementId::LoveYou => Achievement { id: id.clone(), name: "Love Connection".to_string(), description: "Expressed love to Hikari".to_string(), icon: "๐Ÿ’•".to_string(), unlocked_at: None, }, // Personality & Fun AchievementId::EmojiUser => Achievement { id: id.clone(), name: "Emoji Enthusiast".to_string(), description: "Used an emoji in your message".to_string(), icon: "๐Ÿ˜Š".to_string(), unlocked_at: None, }, AchievementId::QuestionMaster => Achievement { id: id.clone(), name: "Question Master".to_string(), description: "Asked 20 questions".to_string(), icon: "โ“".to_string(), unlocked_at: None, }, AchievementId::CapsLock => Achievement { id: id.clone(), name: "CAPS LOCK ENGAGED".to_string(), description: "SENT A MESSAGE IN ALL CAPS".to_string(), icon: "๐Ÿ“ข".to_string(), unlocked_at: None, }, AchievementId::PleaseAndThankYou => Achievement { id: id.clone(), name: "Polite Programmer".to_string(), description: "Said please in a request".to_string(), icon: "๐ŸŽฉ".to_string(), unlocked_at: None, }, // Git & Development AchievementId::GitGuru => Achievement { id: id.clone(), name: "Git Guru".to_string(), description: "Used git commands 10 times".to_string(), icon: "๐ŸŒฟ".to_string(), unlocked_at: None, }, AchievementId::TestWriter => Achievement { id: id.clone(), name: "Test Writer".to_string(), description: "Created test files".to_string(), icon: "๐Ÿงช".to_string(), unlocked_at: None, }, AchievementId::Debugger => Achievement { id: id.clone(), name: "Bug Squasher".to_string(), description: "Fixed bugs and errors".to_string(), icon: "๐Ÿ›".to_string(), unlocked_at: None, }, // Tool Mastery AchievementId::BashMaster => Achievement { id: id.clone(), name: "Bash Master".to_string(), description: "Used Bash tool 50 times".to_string(), icon: "๐Ÿš".to_string(), unlocked_at: None, }, AchievementId::FileExplorer => Achievement { id: id.clone(), name: "File Explorer".to_string(), description: "Read 100 files".to_string(), icon: "๐Ÿ“‚".to_string(), unlocked_at: None, }, AchievementId::SearchExpert => Achievement { id: id.clone(), name: "Search Expert".to_string(), description: "Used Grep tool 50 times".to_string(), icon: "๐Ÿ”Ž".to_string(), unlocked_at: None, }, } } // Check achievements based on message content pub fn check_message_achievements( message: &str, progress: &mut AchievementProgress, ) -> Vec { let mut newly_unlocked = Vec::new(); let message_lower = message.to_lowercase(); println!("Checking message achievements for: {}", message); // Relationship & Greetings if message_lower.contains("good morning") && progress.unlock(AchievementId::GoodMorning) { newly_unlocked.push(AchievementId::GoodMorning); } if (message_lower.contains("good night") || message_lower.contains("goodnight")) && progress.unlock(AchievementId::GoodNight) { newly_unlocked.push(AchievementId::GoodNight); } if (message_lower.contains("thank you") || message_lower.contains("thanks") || message_lower.contains("thx")) && progress.unlock(AchievementId::ThankYou) { newly_unlocked.push(AchievementId::ThankYou); } if (message_lower.contains("love you") || message_lower.contains("ily")) && progress.unlock(AchievementId::LoveYou) { newly_unlocked.push(AchievementId::LoveYou); } // Personality & Fun if message.chars().any(|c| c as u32 >= 0x1F300) && progress.unlock(AchievementId::EmojiUser) { newly_unlocked.push(AchievementId::EmojiUser); } if message == message.to_uppercase() && message.len() > 5 && message.chars().any(|c| c.is_alphabetic()) && progress.unlock(AchievementId::CapsLock) { newly_unlocked.push(AchievementId::CapsLock); } if message_lower.contains("please") && progress.unlock(AchievementId::PleaseAndThankYou) { newly_unlocked.push(AchievementId::PleaseAndThankYou); } // Git & Development patterns in messages if (message_lower.contains("fix") || message_lower.contains("bug") || message_lower.contains("error")) && progress.unlock(AchievementId::Debugger) { newly_unlocked.push(AchievementId::Debugger); } newly_unlocked } // Check which achievements should be unlocked based on current stats pub fn check_achievements( stats: &crate::stats::UsageStats, progress: &mut AchievementProgress, ) -> Vec { let mut newly_unlocked = Vec::new(); println!( "Checking achievements with stats: messages={}, tokens={}, code_blocks={}", stats.messages_exchanged, stats.total_input_tokens + stats.total_output_tokens, stats.code_blocks_generated ); println!("Currently unlocked: {:?}", progress.unlocked); // Token milestones let total_tokens = stats.total_input_tokens + stats.total_output_tokens; if total_tokens >= 1_000 && progress.unlock(AchievementId::FirstSteps) { println!("Unlocked FirstSteps achievement!"); newly_unlocked.push(AchievementId::FirstSteps); } if total_tokens >= 10_000 && progress.unlock(AchievementId::GrowingStrong) { newly_unlocked.push(AchievementId::GrowingStrong); } if total_tokens >= 100_000 && progress.unlock(AchievementId::BlossomingCoder) { newly_unlocked.push(AchievementId::BlossomingCoder); } if total_tokens >= 1_000_000 && progress.unlock(AchievementId::TokenMaster) { newly_unlocked.push(AchievementId::TokenMaster); } // Code generation if stats.code_blocks_generated >= 1 && progress.unlock(AchievementId::HelloWorld) { newly_unlocked.push(AchievementId::HelloWorld); } if stats.code_blocks_generated >= 100 && progress.unlock(AchievementId::CodeWizard) { newly_unlocked.push(AchievementId::CodeWizard); } if stats.code_blocks_generated >= 1000 && progress.unlock(AchievementId::ThousandBlocks) { newly_unlocked.push(AchievementId::ThousandBlocks); } // File operations if stats.files_edited >= 10 && progress.unlock(AchievementId::FileManipulator) { newly_unlocked.push(AchievementId::FileManipulator); } let total_files = stats.files_edited + stats.files_created; if total_files >= 100 && progress.unlock(AchievementId::FileArchitect) { newly_unlocked.push(AchievementId::FileArchitect); } // Conversation milestones if stats.messages_exchanged >= 1 && progress.unlock(AchievementId::FirstMessage) { newly_unlocked.push(AchievementId::FirstMessage); } if stats.messages_exchanged >= 10 && progress.unlock(AchievementId::ConversationStarter) { newly_unlocked.push(AchievementId::ConversationStarter); } if stats.messages_exchanged >= 100 && progress.unlock(AchievementId::ChattyKathy) { newly_unlocked.push(AchievementId::ChattyKathy); } if stats.messages_exchanged >= 1000 && progress.unlock(AchievementId::Conversationalist) { newly_unlocked.push(AchievementId::Conversationalist); } // Tool usage let unique_tools = stats.tools_usage.len(); if unique_tools >= 5 && progress.unlock(AchievementId::Toolsmith) { newly_unlocked.push(AchievementId::Toolsmith); } if unique_tools >= 10 && progress.unlock(AchievementId::ToolMaster) { newly_unlocked.push(AchievementId::ToolMaster); } // Search and exploration let search_tools = ["Glob", "Grep", "search", "Task"]; let search_count: u64 = search_tools .iter() .filter_map(|tool| stats.tools_usage.get(*tool)) .sum(); if search_count >= 50 && progress.unlock(AchievementId::Explorer) { newly_unlocked.push(AchievementId::Explorer); } if search_count >= 500 && progress.unlock(AchievementId::MasterSearcher) { newly_unlocked.push(AchievementId::MasterSearcher); } // Session duration achievements let session_secs = stats.session_duration_seconds; if session_secs < 300 && stats.session_messages_exchanged >= 5 && progress.unlock(AchievementId::QuickSession) { newly_unlocked.push(AchievementId::QuickSession); } if session_secs >= 1800 && progress.unlock(AchievementId::FocusedWork) { newly_unlocked.push(AchievementId::FocusedWork); } if session_secs >= 7200 && progress.unlock(AchievementId::DeepDive) { newly_unlocked.push(AchievementId::DeepDive); } if session_secs >= 18000 && progress.unlock(AchievementId::MarathonSession) { newly_unlocked.push(AchievementId::MarathonSession); } // Session token achievement let session_tokens = stats.session_input_tokens + stats.session_output_tokens; if session_tokens >= 10000 && progress.unlock(AchievementId::MarathonCoder) { newly_unlocked.push(AchievementId::MarathonCoder); } // Special first-time achievements if !stats.tools_usage.is_empty() && progress.unlock(AchievementId::FirstTool) { newly_unlocked.push(AchievementId::FirstTool); } if stats.code_blocks_generated >= 1 && progress.unlock(AchievementId::FirstCodeBlock) { newly_unlocked.push(AchievementId::FirstCodeBlock); } if stats.files_edited >= 1 && progress.unlock(AchievementId::FirstFileEdit) { newly_unlocked.push(AchievementId::FirstFileEdit); } // Speed coder - need to track time for this // TODO: Implement tracking for 10 code blocks in 10 minutes // Polyglot - need to track languages // TODO: Implement tracking for multiple programming languages // Claude Connoisseur - check model usage // TODO: Track different Claude models used // Tool mastery achievements if let Some(bash_count) = stats.tools_usage.get("Bash") { if *bash_count >= 50 && progress.unlock(AchievementId::BashMaster) { newly_unlocked.push(AchievementId::BashMaster); } } if let Some(read_count) = stats.tools_usage.get("Read") { if *read_count >= 100 && progress.unlock(AchievementId::FileExplorer) { newly_unlocked.push(AchievementId::FileExplorer); } } if let Some(grep_count) = stats.tools_usage.get("Grep") { if *grep_count >= 50 && progress.unlock(AchievementId::SearchExpert) { newly_unlocked.push(AchievementId::SearchExpert); } } // Git Guru - check git command usage in Bash if let Some(bash_count) = stats.tools_usage.get("Bash") { if *bash_count >= 10 && progress.unlock(AchievementId::GitGuru) { // TODO: More specific git command tracking newly_unlocked.push(AchievementId::GitGuru); } } // Time-based achievements if let Some(session_start) = progress.session_start { let hour = session_start.hour(); let weekday = session_start.weekday(); // Early bird - 5 AM to 7 AM if (5..=7).contains(&hour) && progress.unlock(AchievementId::EarlyBird) { newly_unlocked.push(AchievementId::EarlyBird); } // Night owl - after midnight let current_hour = Utc::now().hour(); if current_hour < 6 && progress.unlock(AchievementId::NightOwl) { newly_unlocked.push(AchievementId::NightOwl); } // All nighter - 2 AM to 5 AM if (2..=5).contains(¤t_hour) && progress.unlock(AchievementId::AllNighter) { newly_unlocked.push(AchievementId::AllNighter); } // Weekend warrior use chrono::Weekday; if (weekday == Weekday::Sat || weekday == Weekday::Sun) && progress.unlock(AchievementId::WeekendWarrior) { newly_unlocked.push(AchievementId::WeekendWarrior); } } // Dedicated Developer - need to track consecutive days // TODO: Implement 30 days in a row tracking newly_unlocked } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AchievementUnlockedEvent { pub achievement: Achievement, } // Save achievements to persistent store pub async fn save_achievements( app: &tauri::AppHandle, progress: &AchievementProgress, ) -> Result<(), String> { let store = app.store("achievements.json").map_err(|e| e.to_string())?; // Create a serializable version with just the unlocked achievement IDs let unlocked_list: Vec = progress.unlocked.iter().cloned().collect(); println!("Saving achievements: {:?}", unlocked_list); store.set( "unlocked", serde_json::to_value(unlocked_list).map_err(|e| e.to_string())?, ); store.save().map_err(|e| e.to_string())?; println!("Achievements saved successfully"); Ok(()) } // Load achievements from persistent store pub async fn load_achievements(app: &tauri::AppHandle) -> AchievementProgress { println!("Loading achievements from store..."); let store = match app.store("achievements.json") { Ok(s) => s, Err(e) => { println!("Failed to open achievements store: {}", e); return AchievementProgress::new(); } }; let mut progress = AchievementProgress::new(); // Get unlocked achievements if let Some(unlocked_value) = store.get("unlocked") { println!("Found unlocked value in store: {:?}", unlocked_value); if let Ok(unlocked_list) = serde_json::from_value::>(unlocked_value.clone()) { println!("Loaded {} achievements", unlocked_list.len()); for achievement_id in unlocked_list { progress.unlocked.insert(achievement_id); } } else { println!("Failed to parse unlocked achievements"); } } else { println!("No unlocked achievements found in store"); } progress } #[cfg(test)] mod tests { use super::*; #[test] fn test_achievement_unlock() { let mut progress = AchievementProgress::new(); // First unlock should return true assert!(progress.unlock(AchievementId::FirstSteps)); assert!(progress.is_unlocked(&AchievementId::FirstSteps)); // Second unlock of same achievement should return false assert!(!progress.unlock(AchievementId::FirstSteps)); // Newly unlocked should contain the achievement let newly = progress.take_newly_unlocked(); assert_eq!(newly.len(), 1); assert_eq!(newly[0], AchievementId::FirstSteps); // After taking, newly unlocked should be empty let newly = progress.take_newly_unlocked(); assert!(newly.is_empty()); } }