use rusqlite::Connection; use crate::db; pub fn check_achievements(db: &Connection, token: &str) -> Vec { let mut newly_unlocked = Vec::new(); let total = db::get_solve_count(db, token); let (streak_current, streak_best, _) = db::get_streak(db, token); let no_hint = db::get_no_hint_count(db, token); let best = streak_current.max(streak_best); for (count, id) in [(1, "first_solve"), (10, "solver_10"), (25, "solver_25"), (50, "solver_50"), (100, "centurion"), (250, "dedicated"), (500, "obsessed"), (1000, "grandmaster")] { if total >= count && db::unlock_achievement(db, token, id) { newly_unlocked.push(id.to_string()); } } for (count, id) in [(3, "streak_3"), (7, "streak_7"), (14, "streak_14"), (30, "streak_30"), (60, "streak_60"), (100, "streak_100"), (365, "streak_365")] { if best >= count && db::unlock_achievement(db, token, id) { newly_unlocked.push(id.to_string()); } } for (count, id) in [(1, "purist"), (10, "purist_10"), (50, "purist_50")] { if no_hint >= count && db::unlock_achievement(db, token, id) { newly_unlocked.push(id.to_string()); } } for (preset, id) in [("glyph", "glyph_first"), ("rune", "rune_first"), ("scroll", "scroll_first"), ("codex", "codex_first"), ("grimoire", "grimoire_first")] { if db::get_solve_count_by_preset(db, token, preset) >= 1 { if db::unlock_achievement(db, token, id) { newly_unlocked.push(id.to_string()); } } } let gly = db::get_solve_count_by_preset(db, token, "glyph"); let run = db::get_solve_count_by_preset(db, token, "rune"); let scr = db::get_solve_count_by_preset(db, token, "scroll"); let cod = db::get_solve_count_by_preset(db, token, "codex"); let gri = db::get_solve_count_by_preset(db, token, "grimoire"); if gly >= 1 && run >= 1 && scr >= 1 && cod >= 1 && gri >= 1 { if db::unlock_achievement(db, token, "explorer") { newly_unlocked.push("explorer".to_string()); } } if gly >= 10 && run >= 10 && scr >= 10 && cod >= 10 && gri >= 10 { if db::unlock_achievement(db, token, "true_master") { newly_unlocked.push("true_master".to_string()); } } if let Some(t) = db::get_fastest_solve(db, token, "glyph") { if t <= 30 { if db::unlock_achievement(db, token, "speed_glyph") { newly_unlocked.push("speed_glyph".to_string()); } } } if let Some(t) = db::get_fastest_solve(db, token, "rune") { if t <= 60 { if db::unlock_achievement(db, token, "speed_rune") { newly_unlocked.push("speed_rune".to_string()); } } if t <= 30 { if db::unlock_achievement(db, token, "lightning_rune") { newly_unlocked.push("lightning_rune".to_string()); } } } if let Some(t) = db::get_fastest_solve(db, token, "scroll") { if t <= 180 { if db::unlock_achievement(db, token, "speed_scroll") { newly_unlocked.push("speed_scroll".to_string()); } } } if let Some(t) = db::get_fastest_solve(db, token, "codex") { if t <= 600 { if db::unlock_achievement(db, token, "speed_codex") { newly_unlocked.push("speed_codex".to_string()); } } } newly_unlocked } /// (id, name, description, icon, color1, color2) /// Each achievement has a UNIQUE filled icon. pub fn all_achievements() -> Vec<(&'static str, &'static str, &'static str, &'static str, &'static str, &'static str)> { vec![ // milestones - progression through puzzle counts ("first_solve", "First Steps", "Complete your first puzzle", "puzzle-filled", "#4ade80", "#22c55e"), ("solver_10", "Getting Started", "Complete 10 puzzles", "star-filled", "#60a5fa", "#3b82f6"), ("solver_25", "Quarter Century", "Complete 25 puzzles", "badge-filled", "#a78bfa", "#8b5cf6"), ("solver_50", "Half Century", "Complete 50 puzzles", "award-filled", "#f472b6", "#ec4899"), ("centurion", "Centurion", "Complete 100 puzzles", "trophy-filled", "#fbbf24", "#f59e0b"), ("dedicated", "Dedicated", "Complete 250 puzzles", "diamond-filled", "#fb923c", "#f97316"), ("obsessed", "Obsessed", "Complete 500 puzzles", "heart-filled", "#f87171", "#ef4444"), ("grandmaster", "Grandmaster", "Complete 1000 puzzles", "crown-filled", "#e879f9", "#d946ef"), // streaks - consecutive days ("streak_3", "Hat Trick", "3-day streak", "flame-filled", "#4ade80", "#22c55e"), ("streak_7", "Full Week", "7-day streak", "sunrise-filled", "#60a5fa", "#3b82f6"), ("streak_14", "Fortnight", "14-day streak", "calendar-filled", "#a78bfa", "#8b5cf6"), ("streak_30", "Monthly", "30-day streak", "moon-filled", "#fbbf24", "#f59e0b"), ("streak_60", "Iron Will", "60-day streak", "shield-filled", "#fb923c", "#f97316"), ("streak_100", "Unstoppable", "100-day streak", "meteor-filled", "#f87171", "#ef4444"), ("streak_365", "Year Strong", "365-day streak", "globe-filled", "#e879f9", "#d946ef"), // purist - no hints ("purist", "Purist", "Solve without hints", "shield-check-filled", "#4ade80", "#22c55e"), ("purist_10", "True Purist", "10 solves without hints", "eye-filled", "#60a5fa", "#3b82f6"), ("purist_50", "Unassisted", "50 solves without hints", "atom-filled", "#fbbf24", "#f59e0b"), // presets - difficulty levels ("glyph_first", "First Glyph", "Complete a Glyph puzzle", "circle-filled", "#d9f99d", "#bef264"), ("rune_first", "Rune Reader", "Complete a Rune puzzle", "circle-filled", "#86efac", "#4ade80"), ("scroll_first", "Scroll Keeper", "Complete a Scroll puzzle", "square-filled", "#93c5fd", "#60a5fa"), ("codex_first", "Codex Breaker", "Complete a Codex puzzle", "hexagon-filled", "#c4b5fd", "#a78bfa"), ("grimoire_first", "Grimoire Master", "Complete a Grimoire puzzle", "octagon-filled", "#fde68a", "#fbbf24"), ("explorer", "Explorer", "One of each difficulty", "compass-filled", "#f0abfc", "#e879f9"), ("true_master", "True Master", "10 of each difficulty", "rosette-filled", "#fbbf24", "#f59e0b"), // speed - fast solves ("speed_glyph", "Quick Sketch", "Glyph under 30s", "bolt-filled", "#d9f99d", "#bef264"), ("speed_rune", "Speed Demon", "Rune under 60s", "bolt-filled", "#fbbf24", "#f59e0b"), ("lightning_rune", "Lightning", "Rune under 30s", "sparkles-filled", "#f87171", "#ef4444"), ("speed_scroll", "Quick Thinker", "Scroll under 3 min", "clock-filled", "#fb923c", "#f97316"), ("speed_codex", "Sharp Mind", "Codex under 10 min", "bulb-filled", "#a78bfa", "#8b5cf6"), ] }