// The achievements page: every achievement with its badge (bronze, silver, gold, ... in the // colour of its tier), name, description and the progress towards it. Hidden ones show "???" // until they are unlocked. use super::*; use crate::achievements::{all, texts, TIERS}; const TIER_COL: [(f32, f32, f32); 5] = [(0.72, 0.45, 0.25), (0.75, 0.78, 0.85), (0.95, 0.78, 0.25), (0.55, 0.9, 1.0), (0.85, 0.5, 1.0)]; impl Menu { pub(super) fn draw_achievements(&mut self, gfx: &Gfx) { let sw = sw(); let sh = sh(); let cols = 70.min((sw / CW()) as i32 - 2); let rows = 40.min((sh / CH()) as i32 - 2); let x = grid_center(sw, cols as f32 * CW()).max(0.0); let y = grid_center_y(sh, rows as f32 * CH()).max(0.0); let title = if self.ach_hall { tr("hall.title") } else { tr("menu.achievements") }; gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &title, FRAME_HI); if self.ach_hall { self.draw_hall_page(gfx, x, y, cols, rows); self.ach_buttons(gfx, x, y, rows); return; } let list = all(); let done = list.iter().filter(|a| self.profile.unlocked.contains_key(&a.0)).count(); let head = trf("ach.count", &[done.to_string(), list.len().to_string(), crate::achievements::fmt_count(self.profile.total("minutes"))]); gfx.text(&head, x + CW() * 3.0, y + CH() * 1.6, S(), GOLD); let top = y + CH() * 3.5; let bottom = y + (rows - 4) as f32 * CH(); let row_h = CH() * 3.2; self.rows_visible = (((bottom - top) / row_h).floor() as usize).max(1); self.scroll = self.scroll.min(list.len().saturating_sub(self.rows_visible)); let ix = x + CW() * 3.0; let w = (cols - 6) as f32 * CW(); let maxc = ((w - CW() * 6.0) / (GW * SMALL())) as usize; for (k, (id, hidden, icon, tier)) in list.iter().enumerate().skip(self.scroll).take(self.rows_visible) { let ry = top + (k - self.scroll) as f32 * row_h; let got = self.profile.unlocked.contains_key(id); // the badge: a frame in the tier's colour around the icon (dark while locked) let (r, g, b) = TIER_COL[(*tier).min(4)]; let bc = if got { Color::new(r, g, b, 1.0) } else { Color::new(0.25, 0.22, 0.22, 1.0) }; let pic = CH() * 2.6; draw_rectangle(ix, ry, pic, pic, Color::new(0.08, 0.05, 0.05, 1.0)); draw_rectangle_lines(ix, ry, pic, pic, 3.0, bc); let tint = if got { WHITE } else { Color::new(0.3, 0.3, 0.3, 1.0) }; draw_texture_ex(&gfx.badges, ix + 4.0, ry + 4.0, tint, DrawTextureParams { source: Some(crate::badges::badge_rect(*icon)), dest_size: Some(vec2(pic - 8.0, pic - 8.0)), ..Default::default() }); let tx = ix + pic + CW(); let (name, desc) = if *hidden && !got { ("???".to_string(), tr("ach.hidden")) } else { texts(id) }; gfx.text(&name, tx, ry + CH() * 0.1, S(), if got { GOLD } else { TXT }); let d: String = desc.chars().take(maxc).collect(); gfx.text(&d, tx, ry + CH() * 1.3, SMALL(), DIM); // progress towards a tier if let Some((cat, t)) = id.split_once('.') { if !got { let need = TIERS.iter().find(|x| x.0 == cat).and_then(|x| x.1.get(t.parse::().unwrap_or(1) - 1)).copied().unwrap_or(1.0); let have = self.profile.total(cat); let bar_x = x + (cols - 20) as f32 * CW(); crate::ui::ascii_bar(gfx, bar_x, ry + CH() * 0.1, 10, (have / need).min(1.0) as f32, FRAME_HI, SMALL()); let p = format!("{}/{}", crate::achievements::fmt_count(have.floor()), crate::achievements::fmt_count(need)); gfx.text(&p, bar_x, ry + CH() * 1.3, SMALL(), DIM); } } } // scrolling and back let _ = ix; self.ach_buttons(gfx, x, y, rows); } // scrolling, the switch between achievements and the hall, back fn ach_buttons(&mut self, gfx: &Gfx, x: f32, y: f32, rows: i32) { let ix = x + CW() * 3.0; let by = y + (rows - 3) as f32 * CH(); let (mx, my) = mouse(); let bw = 14.0 * CW(); let switch = if self.ach_hall { tr("menu.achievements") } else { tr("hall.title") }; let items = [(Btn::Scroll(-1), "▲".to_string()), (Btn::Scroll(1), "▼".to_string()), (Btn::AchHall, switch), (Btn::Back, tr("menu.back"))]; let bw = bw.min((sw() - ix - CW() * 4.0) / 4.0 - CW()); for (k, (b, label)) in items.iter().enumerate() { let r = Rect::new(ix + k as f32 * (bw + CW()), by, bw, CH() * 2.0); button(gfx, r, label, r.contains(vec2(mx, my)), FRAME); self.buttons.push((r, *b)); } } // the hall of the fallen over all games: newest first, two lines each fn draw_hall_page(&mut self, gfx: &Gfx, x: f32, y: f32, cols: i32, rows: i32) { let hall = &self.profile.hall; gfx.text(&trf("hall.count", &[hall.len().to_string()]), x + CW() * 3.0, y + CH() * 1.6, S(), GOLD); let top = y + CH() * 3.5; let bottom = y + (rows - 4) as f32 * CH(); let row_h = CH() * 2.4; self.rows_visible = (((bottom - top) / row_h).floor() as usize).max(1); self.scroll = self.scroll.min(hall.len().saturating_sub(self.rows_visible)); let ix = x + CW() * 3.0; let maxc = (cols - 6) as usize; if hall.is_empty() { gfx.text(&tr("hall.empty"), ix, top, S(), DIM); } for (k, f) in hall.iter().rev().enumerate().skip(self.scroll).take(self.rows_visible) { let ry = top + (k - self.scroll) as f32 * row_h; let name = format!("{} ({})", crate::names::full_name(f.kind, f.name, f.level), trf("info.level", &[tr(crate::game::st(f.kind).name), f.level.to_string()])); gfx.text(&name.chars().take(maxc).collect::(), ix, ry, S(), FRAME_HI); let kills = if f.kills > 0 { format!(" · {}", trf("hall.kills", &[f.kills.to_string()])) } else { String::new() }; let line = format!("{}{} · {}", crate::names::cause(f.bane), kills, date(f.when)); gfx.text(&line.chars().take(maxc * 2).collect::(), ix + CW() * 2.0, ry + CH(), SMALL(), DIM); } } } // a unix time as a date (year-month-day) fn date(unix: u64) -> String { if unix == 0 { return String::new(); } // days since 1970-01-01 to a civil date (Howard Hinnant's algorithm) let z = (unix / 86400) as i64 + 719468; let era = z.div_euclid(146097); let doe = z - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 }; format!("{:04}-{:02}-{:02}", y, m, d) } #[cfg(test)] mod tests { #[test] fn dates_are_civil() { assert_eq!(super::date(0), ""); assert_eq!(super::date(1_798_675_200), "2026-12-31"); assert_eq!(super::date(1_709_164_800), "2024-02-29"); } }