diff --git a/cruciverb-server/src/achievements.rs b/cruciverb-server/src/achievements.rs new file mode 100644 index 0000000..2f2cac6 --- /dev/null +++ b/cruciverb-server/src/achievements.rs @@ -0,0 +1,115 @@ +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"), + ] +}