From fdce6c282b3d47bb2cbcc892495c2ca36b2f50bc Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Fri, 6 Nov 2026 10:20:41 +0100 Subject: [PATCH] Give every achievement its own pixel-art badge Co-Authored-By: Claude Opus 5.5 --- src/achievements.rs | 72 +-- src/badges.rs | 929 +++++++++++++++++++++++++++++++++++++++ src/gfx.rs | 5 +- src/main.rs | 1 + src/menu/achievements.rs | 2 +- 5 files changed, 976 insertions(+), 33 deletions(-) create mode 100644 src/badges.rs diff --git a/src/achievements.rs b/src/achievements.rs index 224b1a5..faf3d6e 100644 --- a/src/achievements.rs +++ b/src/achievements.rs @@ -15,40 +15,40 @@ pub const STAT_NAMES: [&str; k::COUNTERS] = [ "heroes", "bosses", "lost", "dug", "wood", "walls", "rooms", "spells", "slaps", "traps", "towers", "drowned", "burnt", "siege", "captured", "gold", "drawbridge", ]; -// tiered achievements: id (= counter), thresholds, icon cell +// tiered achievements: id (= counter), thresholds, badge (badges.rs) pub const TIERS: [(&str, &[f64], u32); 20] = [ - ("heroes", &[1.0, 50.0, 250.0, 1000.0], crate::gfx::I_ARROW), - ("bosses", &[1.0, 5.0, 20.0], crate::gfx::I_RUNE), - ("dug", &[100.0, 1000.0, 10000.0], crate::gfx::I_PICK), - ("gold", &[10000.0, 100000.0, 1000000.0], crate::gfx::I_SELL), - ("wood", &[50.0, 500.0, 2000.0], crate::gfx::I_BRIDGE), - ("walls", &[10.0, 100.0], crate::gfx::I_WALL), - ("rooms", &[50.0, 500.0, 2000.0], crate::gfx::I_STEEL), - ("spells", &[10.0, 100.0, 500.0], crate::gfx::I_BOLT), - ("slaps", &[10.0, 100.0, 1000.0], crate::gfx::I_HAND), - ("traps", &[5.0, 50.0, 200.0], crate::gfx::I_TRAPS), - ("towers", &[5.0, 50.0], crate::gfx::I_CANNON), - ("drowned", &[1.0, 25.0, 100.0], crate::gfx::I_FREEZE), - ("burnt", &[5.0, 50.0], crate::gfx::I_DRAWBRIDGE), - ("siege", &[1.0, 10.0], crate::gfx::I_CATAPULT), - ("captured", &[1.0, 25.0, 100.0], crate::gfx::I_SUMMON), - ("drawbridge", &[1.0, 50.0], crate::gfx::I_DRAWBRIDGE), - ("wins", &[1.0, 10.0, 50.0], crate::gfx::I_FLAG), - ("missions", &[1.0, 10.0, 30.0], crate::gfx::I_MAGIC), - ("coop", &[1.0, 10.0, 20.0], crate::gfx::I_HEAL), - ("minutes", &[60.0, 600.0, 3000.0], crate::gfx::I_SPELLS), + ("heroes", &[1.0, 50.0, 250.0, 1000.0], crate::badges::HEROES), + ("bosses", &[1.0, 5.0, 20.0], crate::badges::BOSSES), + ("dug", &[100.0, 1000.0, 10000.0], crate::badges::DUG), + ("gold", &[10000.0, 100000.0, 1000000.0], crate::badges::GOLD), + ("wood", &[50.0, 500.0, 2000.0], crate::badges::WOOD), + ("walls", &[10.0, 100.0], crate::badges::WALLS), + ("rooms", &[50.0, 500.0, 2000.0], crate::badges::ROOMS), + ("spells", &[10.0, 100.0, 500.0], crate::badges::SPELLS), + ("slaps", &[10.0, 100.0, 1000.0], crate::badges::SLAPS), + ("traps", &[5.0, 50.0, 200.0], crate::badges::TRAPS), + ("towers", &[5.0, 50.0], crate::badges::TOWERS), + ("drowned", &[1.0, 25.0, 100.0], crate::badges::DROWNED), + ("burnt", &[5.0, 50.0], crate::badges::BURNT), + ("siege", &[1.0, 10.0], crate::badges::SIEGE), + ("captured", &[1.0, 25.0, 100.0], crate::badges::CAPTURED), + ("drawbridge", &[1.0, 50.0], crate::badges::DRAWBRIDGE), + ("wins", &[1.0, 10.0, 50.0], crate::badges::WINS), + ("missions", &[1.0, 10.0, 30.0], crate::badges::MISSIONS), + ("coop", &[1.0, 10.0, 20.0], crate::badges::COOP), + ("minutes", &[60.0, 600.0, 3000.0], crate::badges::MINUTES), ]; -// special achievements: id, hidden until unlocked, icon cell +// special achievements: id, hidden until unlocked, badge (badges.rs) pub const SPECIAL: [(&str, bool, u32); 8] = [ - ("flawless", false, crate::gfx::I_HEAL), - ("speedrun", false, crate::gfx::I_BOLT), - ("campaign", false, crate::gfx::I_RUNE), - ("surface", false, crate::gfx::I_WALL), - ("defeat", true, crate::gfx::I_SELL), - ("legend", false, crate::gfx::I_ARROW), - ("wyrm", true, crate::gfx::I_SUMMON), - ("fullhand", true, crate::gfx::I_HAND), + ("flawless", false, crate::badges::FLAWLESS), + ("speedrun", false, crate::badges::SPEEDRUN), + ("campaign", false, crate::badges::CAMPAIGN), + ("surface", false, crate::badges::SURFACE), + ("defeat", true, crate::badges::DEFEAT), + ("legend", false, crate::badges::LEGEND), + ("wyrm", true, crate::badges::WYRM), + ("fullhand", true, crate::badges::FULLHAND), ]; // number of achievements in all @@ -244,7 +244,7 @@ pub fn fmt_count(v: f64) -> String { out } -// every achievement in display order: (id, hidden, icon, tier 0..) +// every achievement in display order: (id, hidden, badge, tier 0..) pub fn all() -> Vec<(String, bool, u32, usize)> { let mut v = Vec::new(); for (id, steps, icon) in TIERS.iter() { @@ -268,6 +268,16 @@ mod tests { assert_eq!(all().len(), count()); } + #[test] + fn every_achievement_has_its_own_badge() { + let mut seen = std::collections::BTreeSet::new(); + for b in TIERS.iter().map(|t| t.2).chain(SPECIAL.iter().map(|s| s.2)) { + assert!(b < crate::badges::COUNT, "badge {} out of range", b); + assert!(seen.insert(b), "badge {} used twice", b); + } + assert_eq!(seen.len() as u32, crate::badges::COUNT); + } + #[test] fn the_tracker_counts_a_game_and_unlocks() { let mut g = Game::new(3, 64, 64); diff --git a/src/badges.rs b/src/badges.rs new file mode 100644 index 0000000..d76ebab --- /dev/null +++ b/src/badges.rs @@ -0,0 +1,929 @@ +// Achievement badges: one 32x32 pixel-art symbol per achievement, generated at start into a +// texture of its own (the main atlas is full). Each symbol is painted from simple shapes in +// materials (steel, gold, wood, ...); every material has a ramp of four shades, and the shapes +// are bevelled automatically (lit from the top left) and get a dark outline. + +use macroquad::prelude::*; + +pub const SIZE: u32 = 32; +const COLS: u32 = 8; + +// badge indices (the icon field of achievements::TIERS / SPECIAL) +pub const HEROES: u32 = 0; +pub const BOSSES: u32 = 1; +pub const DUG: u32 = 2; +pub const GOLD: u32 = 3; +pub const WOOD: u32 = 4; +pub const WALLS: u32 = 5; +pub const ROOMS: u32 = 6; +pub const SPELLS: u32 = 7; +pub const SLAPS: u32 = 8; +pub const TRAPS: u32 = 9; +pub const TOWERS: u32 = 10; +pub const DROWNED: u32 = 11; +pub const BURNT: u32 = 12; +pub const SIEGE: u32 = 13; +pub const CAPTURED: u32 = 14; +pub const DRAWBRIDGE: u32 = 15; +pub const WINS: u32 = 16; +pub const MISSIONS: u32 = 17; +pub const COOP: u32 = 18; +pub const MINUTES: u32 = 19; +pub const FLAWLESS: u32 = 20; +pub const SPEEDRUN: u32 = 21; +pub const CAMPAIGN: u32 = 22; +pub const SURFACE: u32 = 23; +pub const DEFEAT: u32 = 24; +pub const LEGEND: u32 = 25; +pub const WYRM: u32 = 26; +pub const FULLHAND: u32 = 27; +pub const COUNT: u32 = 28; + +// the source rectangle of a badge in the badge texture +pub fn badge_rect(i: u32) -> Rect { + Rect::new(((i % COLS) * SIZE) as f32, ((i / COLS) * SIZE) as f32, SIZE as f32, SIZE as f32) +} + +// ------------------------------------------------------------------ materials + +const EMPTY: u8 = 0; +const STEEL: u8 = 1; +const GOLDM: u8 = 2; +const WOODM: u8 = 3; +const STONE: u8 = 4; +const RED: u8 = 5; +const FIRE: u8 = 6; +const WATER: u8 = 7; +const CRYSTAL: u8 = 8; +const GREEN: u8 = 9; +const BONE: u8 = 10; +const PURPLE: u8 = 11; +const IRON: u8 = 12; // dark iron, also slits and holes +const BRICK: u8 = 13; +const SKIN: u8 = 14; // imp green (the keeper's hand) +const DKWOOD: u8 = 15; +const GLASS: u8 = 16; +const CYAN: u8 = 17; // magic glow +const SUN: u8 = 18; +const BLUE: u8 = 19; +const SPARK: u8 = 20; // white sparkles and motion lines, no outline +const EMBER: u8 = 21; // floating embers and steam, no outline +const TEA: u8 = 22; + +fn ramp(m: u8) -> [(u8, u8, u8); 4] { + match m { + STEEL => [(66, 72, 90), (116, 124, 144), (166, 174, 190), (222, 228, 238)], + GOLDM => [(124, 76, 22), (196, 138, 38), (238, 192, 70), (255, 240, 150)], + WOODM => [(78, 46, 26), (120, 76, 42), (156, 106, 62), (196, 146, 92)], + STONE => [(58, 54, 64), (90, 84, 96), (124, 118, 130), (160, 154, 166)], + RED => [(96, 18, 24), (156, 34, 36), (204, 62, 50), (240, 122, 92)], + FIRE => [(196, 54, 18), (240, 124, 30), (255, 192, 60), (255, 244, 170)], + WATER => [(22, 44, 96), (38, 84, 148), (66, 134, 198), (150, 200, 240)], + CRYSTAL => [(60, 34, 120), (110, 66, 196), (164, 120, 244), (226, 206, 255)], + GREEN => [(34, 66, 30), (58, 108, 42), (98, 150, 58), (152, 202, 92)], + BONE => [(120, 104, 84), (178, 160, 128), (216, 202, 170), (244, 236, 214)], + PURPLE => [(50, 26, 60), (88, 42, 100), (128, 68, 140), (176, 110, 182)], + IRON => [(22, 18, 24), (40, 34, 42), (62, 54, 64), (88, 80, 90)], + BRICK => [(88, 36, 30), (134, 60, 44), (170, 88, 62), (204, 126, 90)], + SKIN => [(56, 84, 44), (98, 138, 72), (140, 184, 102), (184, 222, 140)], + DKWOOD => [(50, 30, 20), (78, 48, 30), (104, 68, 42), (134, 94, 60)], + GLASS => [(92, 128, 150), (136, 176, 196), (180, 214, 228), (232, 246, 252)], + CYAN => [(30, 110, 150), (60, 180, 220), (130, 230, 250), (220, 255, 255)], + SUN => [(220, 120, 30), (250, 180, 50), (255, 222, 90), (255, 250, 200)], + BLUE => [(24, 30, 80), (40, 52, 128), (66, 84, 172), (110, 132, 214)], + SPARK => [(200, 200, 210), (236, 236, 244), (255, 255, 255), (255, 255, 255)], + EMBER => [(200, 80, 30), (255, 150, 50), (255, 210, 110), (255, 250, 220)], + TEA => [(70, 36, 20), (104, 58, 30), (134, 82, 44), (170, 112, 66)], + _ => [(0, 0, 0); 4], + } +} + +fn outlined(m: u8) -> bool { + m != EMPTY && m != SPARK && m != EMBER +} + +const OUTLINE: (u8, u8, u8) = (18, 12, 16); +const AUTO: u8 = 255; + +#[derive(Clone, Copy)] +struct P { + m: u8, + s: u8, // shade 0..3, or AUTO (bevelled) +} + +fn a(m: u8) -> P { + P { m, s: AUTO } +} +fn f(m: u8, s: u8) -> P { + P { m, s } +} +const CUT: P = P { m: EMPTY, s: AUTO }; + +// one badge being painted +struct Pad { + m: [u8; 1024], + s: [u8; 1024], +} + +impl Pad { + fn new() -> Pad { + Pad { m: [EMPTY; 1024], s: [AUTO; 1024] } + } + + fn px(&mut self, x: i32, y: i32, p: P) { + if (0..32).contains(&x) && (0..32).contains(&y) { + let i = (y * 32 + x) as usize; + self.m[i] = p.m; + self.s[i] = p.s; + } + } + + fn get(&self, x: i32, y: i32) -> u8 { + if (0..32).contains(&x) && (0..32).contains(&y) { + self.m[(y * 32 + x) as usize] + } else { + EMPTY + } + } + + // every pixel whose centre passes the test + fn fill(&mut self, p: P, test: impl Fn(f32, f32) -> bool) { + for y in 0..32 { + for x in 0..32 { + if test(x as f32 + 0.5, y as f32 + 0.5) { + self.px(x, y, p); + } + } + } + } + + // inclusive pixel rectangle + fn rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, p: P) { + for y in y0..=y1 { + for x in x0..=x1 { + self.px(x, y, p); + } + } + } + + fn disc(&mut self, cx: f32, cy: f32, r: f32, p: P) { + self.fill(p, |x, y| (x - cx).powi(2) + (y - cy).powi(2) <= r * r); + } + + fn ring(&mut self, cx: f32, cy: f32, r0: f32, r1: f32, p: P) { + self.fill(p, |x, y| { + let d = (x - cx).powi(2) + (y - cy).powi(2); + d <= r1 * r1 && d >= r0 * r0 + }); + } + + // ellipse turned by `ang` (radians) + fn oval(&mut self, cx: f32, cy: f32, rx: f32, ry: f32, ang: f32, p: P) { + let (s, c) = ang.sin_cos(); + self.fill(p, |x, y| { + let (dx, dy) = (x - cx, y - cy); + let u = dx * c + dy * s; + let v = -dx * s + dy * c; + (u / rx).powi(2) + (v / ry).powi(2) <= 1.0 + }); + } + + // a thick line (round ends) + fn line(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, w: f32, p: P) { + let (dx, dy) = (x1 - x0, y1 - y0); + let len2 = (dx * dx + dy * dy).max(1e-6); + let r = w * 0.5; + self.fill(p, |x, y| { + let t = (((x - x0) * dx + (y - y0) * dy) / len2).clamp(0.0, 1.0); + let (qx, qy) = (x0 + t * dx - x, y0 + t * dy - y); + qx * qx + qy * qy <= r * r + }); + } + + // a line through several points + fn path(&mut self, pts: &[(f32, f32)], w: f32, p: P) { + for s in pts.windows(2) { + self.line(s[0].0, s[0].1, s[1].0, s[1].1, w, p); + } + } + + fn poly(&mut self, pts: &[(f32, f32)], p: P) { + self.fill(p, |x, y| { + let mut inside = false; + let n = pts.len(); + let mut j = n - 1; + for i in 0..n { + let (xi, yi) = pts[i]; + let (xj, yj) = pts[j]; + if (yi > y) != (yj > y) && x < (xj - xi) * (y - yi) / (yj - yi) + xi { + inside = !inside; + } + j = i; + } + inside + }); + } + + // a five-pointed star + fn star(&mut self, cx: f32, cy: f32, r0: f32, r1: f32, p: P) { + let pts: Vec<(f32, f32)> = (0..10) + .map(|k| { + let ang = -std::f32::consts::FRAC_PI_2 + k as f32 * std::f32::consts::PI / 5.0; + let r = if k % 2 == 0 { r1 } else { r0 }; + (cx + r * ang.cos(), cy + r * ang.sin()) + }) + .collect(); + self.poly(&pts, p); + } + + // a small four-pointed sparkle + fn sparkle(&mut self, x: i32, y: i32) { + let p = f(SPARK, 2); + self.px(x, y, p); + for (dx, dy) in [(1, 0), (-1, 0), (0, 1), (0, -1)] { + self.px(x + dx, y + dy, f(SPARK, 0)); + } + } + + // bevel, outline and write into the image at cell `i` + fn blit(&self, img: &mut Image, i: u32) { + let (ox, oy) = ((i % COLS) * SIZE, (i / COLS) * SIZE); + for y in 0..32i32 { + for x in 0..32i32 { + let m = self.get(x, y); + let col = if m == EMPTY { + let near = [(1, 0), (-1, 0), (0, 1), (0, -1)].iter().any(|(dx, dy)| outlined(self.get(x + dx, y + dy))); + if !near { + continue; + } + OUTLINE + } else { + let mut s = self.s[(y * 32 + x) as usize]; + if s == AUTO { + let lit = self.get(x - 1, y) != m || self.get(x, y - 1) != m; + let dark = self.get(x + 1, y) != m || self.get(x, y + 1) != m; + s = match (lit, dark) { + (true, false) => 2, + (false, true) => 0, + _ => 1, + }; + // the top-left corner catches the light + if lit && !dark && self.get(x - 1, y) != m && self.get(x, y - 1) != m { + s = 3; + } + } + ramp(m)[s.min(3) as usize] + }; + img.set_pixel(ox + x as u32, oy + y as u32, Color::from_rgba(col.0, col.1, col.2, 255)); + } + } + } +} + +// ------------------------------------------------------------------ the symbols + +fn heroes(p: &mut Pad) { + // a knight's great helm, split by a blow + p.poly(&[(16.0, 1.5), (21.0, 4.0), (19.0, 7.0), (16.0, 6.0)], a(RED)); // plume + p.disc(16.0, 13.0, 8.5, a(STEEL)); + p.rect(8, 13, 24, 25, a(STEEL)); + p.rect(9, 14, 23, 15, f(IRON, 0)); // visor slit + p.line(16.0, 6.0, 16.0, 25.0, 1.0, f(STEEL, 0)); // ridge + for (x, y) in [(11, 19), (13, 21), (11, 22), (20, 19), (22, 21)] { + p.px(x, y, f(IRON, 1)); + } + // the crack and the broken-off piece + p.poly(&[(19.0, 3.0), (26.0, 3.0), (26.0, 12.0), (21.0, 9.0)], CUT); + p.path(&[(21.0, 9.0), (18.0, 13.0), (20.0, 17.0), (17.5, 22.0)], 1.0, f(IRON, 0)); + p.poly(&[(23.0, 26.0), (28.0, 24.0), (29.5, 29.5), (24.0, 29.5)], a(STEEL)); + p.line(24.0, 26.5, 28.0, 25.0, 0.8, f(STEEL, 3)); + p.px(27, 7, f(STEEL, 2)); + p.px(29, 11, f(STEEL, 1)); + p.sparkle(28, 18); +} + +fn bosses(p: &mut Pad) { + // a skull wearing a cracked crown + p.disc(16.0, 19.0, 9.0, a(BONE)); + p.rect(11, 24, 21, 29, a(BONE)); + p.disc(12.0, 19.0, 2.6, f(IRON, 0)); + p.disc(20.0, 19.0, 2.6, f(IRON, 0)); + p.px(12, 19, f(RED, 3)); + p.px(20, 19, f(RED, 3)); + p.poly(&[(16.0, 21.5), (14.5, 24.5), (17.5, 24.5)], f(IRON, 0)); + for x in [12, 14, 16, 18, 20] { + p.line(x as f32 + 0.5, 26.0, x as f32 + 0.5, 29.0, 0.8, f(BONE, 0)); + } + p.poly(&[(6.0, 13.0), (6.0, 3.0), (10.5, 8.0), (16.0, 1.0), (21.5, 8.0), (26.0, 3.0), (26.0, 13.0)], a(GOLDM)); + p.rect(6, 11, 25, 13, f(GOLDM, 0)); + p.disc(16.0, 8.5, 1.8, a(RED)); + p.disc(9.0, 10.0, 1.2, f(CRYSTAL, 2)); + p.disc(23.0, 10.0, 1.2, f(CRYSTAL, 2)); + p.path(&[(13.0, 3.0), (14.5, 7.0), (12.5, 10.0), (13.5, 13.5)], 1.2, CUT); +} + +fn dug(p: &mut Pad) { + // a pickaxe struck into a rock + p.poly(&[(2.0, 30.0), (4.0, 21.0), (10.0, 16.0), (18.0, 17.0), (25.0, 20.0), (30.0, 30.0)], a(STONE)); + p.path(&[(8.0, 22.0), (12.0, 25.0), (11.0, 29.0)], 1.0, f(STONE, 0)); + p.path(&[(21.0, 23.0), (24.0, 27.0)], 1.0, f(STONE, 0)); + p.rect(5, 26, 6, 27, a(GOLDM)); + p.rect(16, 27, 17, 28, a(GOLDM)); + p.px(26, 26, f(GOLDM, 2)); + p.line(25.0, 5.0, 13.0, 23.0, 2.6, a(WOODM)); + p.path(&[(10.0, 7.0), (16.0, 3.0), (23.0, 4.0)], 3.2, a(STEEL)); + p.path(&[(23.0, 4.0), (28.0, 9.0), (29.0, 15.0)], 3.0, a(STEEL)); + p.line(9.0, 7.5, 10.0, 7.0, 1.6, a(STEEL)); + p.disc(24.0, 5.5, 2.4, a(IRON)); + // chips flying + p.rect(5, 12, 6, 13, a(STONE)); + p.rect(8, 9, 8, 10, a(STONE)); + p.px(3, 16, f(STONE, 2)); +} + +fn gold(p: &mut Pad) { + // a pile of coins + p.poly(&[(3.0, 29.0), (7.0, 21.0), (13.0, 17.0), (20.0, 17.0), (26.0, 21.0), (29.0, 29.0)], a(GOLDM)); + for (x, y) in [(8, 25), (14, 22), (20, 25), (24, 23), (11, 27), (17, 27)] { + p.oval(x as f32 + 0.5, y as f32, 2.6, 1.2, 0.0, f(GOLDM, 0)); + p.oval(x as f32 + 0.5, y as f32 - 0.5, 2.0, 0.8, 0.0, f(GOLDM, 2)); + } + // a standing coin + p.disc(12.0, 13.0, 7.0, a(GOLDM)); + p.ring(12.0, 13.0, 4.6, 5.6, f(GOLDM, 0)); + p.rect(11, 10, 12, 16, f(GOLDM, 0)); + p.rect(10, 10, 13, 10, f(GOLDM, 0)); + p.rect(10, 16, 13, 16, f(GOLDM, 0)); + // another coin flying + p.oval(24.0, 9.0, 4.0, 4.0, 0.0, a(GOLDM)); + p.ring(24.0, 9.0, 2.0, 2.9, f(GOLDM, 0)); + p.sparkle(27, 16); + p.sparkle(5, 7); +} + +fn wood(p: &mut Pad) { + // an axe buried in a log + p.rect(3, 18, 23, 27, a(WOODM)); + for (x0, x1, y) in [(5, 12, 20), (9, 19, 23), (4, 9, 25), (14, 21, 26)] { + p.rect(x0, y, x1, y, f(WOODM, 0)); + } + p.oval(24.5, 22.5, 4.0, 5.2, 0.0, f(BONE, 1)); + p.ring(24.5, 22.5, 1.5, 2.3, f(WOODM, 2)); + p.ring(24.5, 22.5, 3.2, 3.9, f(WOODM, 2)); + p.px(24, 22, f(WOODM, 1)); + p.line(5.0, 3.0, 15.0, 17.0, 2.6, a(DKWOOD)); + p.poly(&[(10.0, 14.0), (17.0, 11.0), (20.5, 17.5), (13.0, 19.0)], a(STEEL)); + p.line(12.0, 18.6, 20.0, 17.6, 1.0, f(STEEL, 3)); + p.rect(9, 17, 21, 18, a(WOODM)); + p.rect(12, 17, 19, 17, f(IRON, 0)); + // chips + p.rect(23, 9, 24, 10, f(WOODM, 2)); + p.rect(26, 13, 26, 14, f(WOODM, 2)); +} + +fn walls(p: &mut Pad) { + // a brick wall with battlements + p.rect(3, 9, 28, 28, f(IRON, 1)); + for (x0, x1) in [(3, 8), (13, 18), (23, 28)] { + p.rect(x0, 3, x1, 8, f(IRON, 1)); + } + for (row, y) in [8, 13, 18, 23].iter().enumerate() { + let off = if row % 2 == 0 { 0 } else { 4 }; + let mut x = 3 - off; + while x <= 28 { + p.rect(x.max(3), y + 1, (x + 6).min(28), y + 4, a(BRICK)); + x += 8; + } + } + for (x0, x1) in [(3, 8), (13, 18), (23, 28)] { + p.rect(x0, 4, x1, 7, a(BRICK)); + } + p.rect(3, 9, 28, 9, f(IRON, 1)); + // a crack and a loose brick + p.path(&[(19.0, 14.0), (17.0, 18.0), (19.0, 21.0)], 1.0, f(IRON, 0)); +} + +fn rooms(p: &mut Pad) { + // a stone arch with a barred wooden door + p.disc(16.0, 15.0, 13.0, a(STONE)); + p.rect(3, 15, 28, 29, a(STONE)); + p.disc(16.0, 15.0, 8.5, f(IRON, 0)); + p.rect(8, 15, 23, 29, f(IRON, 0)); + for k in 0..7 { + let ang = std::f32::consts::PI * (1.0 + k as f32 / 6.0); + p.line(16.0 + 9.0 * ang.cos(), 15.0 + 9.0 * ang.sin(), 16.0 + 13.0 * ang.cos(), 15.0 + 13.0 * ang.sin(), 0.9, f(STONE, 0)); + } + p.rect(3, 22, 7, 22, f(STONE, 0)); + p.rect(24, 22, 28, 22, f(STONE, 0)); + p.disc(16.0, 15.5, 7.5, a(WOODM)); + p.rect(9, 15, 22, 29, a(WOODM)); + for x in [12, 16, 19] { + p.line(x as f32 + 0.5, 8.5, x as f32 + 0.5, 29.0, 0.9, f(WOODM, 0)); + } + p.rect(9, 13, 22, 14, a(IRON)); + p.rect(9, 23, 22, 24, a(IRON)); + p.ring(20.0, 19.0, 1.2, 2.2, a(GOLDM)); + p.rect(2, 29, 29, 30, a(STONE)); +} + +fn spells(p: &mut Pad) { + // an open spell book with a swirl of magic + p.poly(&[(2.0, 15.0), (16.0, 19.0), (30.0, 15.0), (30.0, 28.0), (16.0, 31.0), (2.0, 28.0)], a(PURPLE)); + p.poly(&[(3.5, 13.0), (15.5, 17.0), (15.5, 28.5), (3.5, 25.5)], a(BONE)); + p.poly(&[(16.5, 17.0), (28.5, 13.0), (28.5, 25.5), (16.5, 28.5)], a(BONE)); + for k in 0..3 { + let y = 18.0 + k as f32 * 3.0; + p.line(5.5, y, 13.5, y + 2.4, 0.8, f(BONE, 0)); + p.line(18.5, y + 2.4, 26.5, y, 0.8, f(BONE, 0)); + } + p.px(5, 16, f(RED, 2)); + p.px(6, 16, f(RED, 2)); + // the swirl rising from the pages + let mut prev = (16.0, 15.0); + for k in 1..22 { + let t = k as f32 / 21.0; + let ang = t * 9.0; + let r = 1.0 + t * 6.5; + let pt = (16.0 + r * ang.cos(), 14.0 - t * 8.0 + r * 0.5 * ang.sin()); + p.line(prev.0, prev.1, pt.0, pt.1, 1.0 + t * 1.2, f(CYAN, if t > 0.6 { 1 } else { 2 })); + prev = pt; + } + p.disc(16.0, 9.0, 1.4, f(CYAN, 3)); + p.sparkle(5, 4); + p.sparkle(27, 5); + p.sparkle(26, 10); +} + +fn slaps(p: &mut Pad) { + // the keeper's open hand, swinging + let tilt = 0.25f32; + let (s, c) = tilt.sin_cos(); + let rot = |x: f32, y: f32| (19.0 + (x - 19.0) * c - (y - 19.0) * s, 19.0 + (x - 19.0) * s + (y - 19.0) * c); + let fingers = [(13.5, 6.5), (17.0, 4.5), (20.5, 5.0), (24.0, 8.0)]; + for (x, y) in fingers { + let (x0, y0) = rot(x, 18.0); + let (x1, y1) = rot(x, y); + p.line(x0, y0, x1, y1, 3.2, a(SKIN)); + p.px((x1 - 0.5) as i32, (y1 - 1.6) as i32, f(BONE, 2)); + } + let (px0, py0) = rot(19.0, 20.0); + p.oval(px0, py0, 6.5, 7.0, tilt, a(SKIN)); + let (tx0, ty0) = rot(13.5, 21.0); + let (tx1, ty1) = rot(8.5, 15.0); + p.line(tx0, ty0, tx1, ty1, 3.2, a(SKIN)); + p.px((tx1 - 1.0) as i32, (ty1 - 1.0) as i32, f(BONE, 2)); + let (wx0, wy0) = rot(17.0, 26.0); + let (wx1, wy1) = rot(17.0, 31.0); + p.line(wx0, wy0, wx1, wy1, 6.0, a(SKIN)); + p.line(wx0 - 3.0, wy0 + 2.5, wx0 + 3.5, wy0 + 1.0, 1.6, a(PURPLE)); // sleeve cuff + p.line(15.5, 15.0, 19.5, 22.0, 0.8, f(SKIN, 0)); + // motion lines + for (x0, y, x1) in [(1, 9, 6), (2, 14, 7), (1, 19, 5)] { + p.rect(x0, y, x1, y, f(SPARK, 1)); + } +} + +fn traps(p: &mut Pad) { + // spikes shooting from a floor slab, one of them bloodied + p.rect(2, 23, 29, 29, a(STONE)); + p.rect(2, 26, 29, 26, f(STONE, 0)); + p.rect(15, 23, 15, 26, f(STONE, 0)); + p.rect(8, 26, 8, 29, f(STONE, 0)); + p.rect(23, 26, 23, 29, f(STONE, 0)); + for (cx, tip) in [(6.0, 11.0), (11.0, 7.0), (16.0, 3.0), (21.0, 7.0), (26.0, 11.0)] { + p.poly(&[(cx - 2.6, 23.0), (cx, tip), (cx + 2.6, 23.0)], a(STEEL)); + p.line(cx - 0.6, tip + 3.0, cx - 1.6, 22.0, 0.8, f(STEEL, 3)); + p.rect(cx as i32 - 3, 22, cx as i32 + 2, 23, f(IRON, 1)); + } + p.poly(&[(14.6, 8.0), (16.0, 3.0), (17.4, 8.0)], a(RED)); + p.rect(16, 9, 16, 11, f(RED, 1)); + p.px(16, 13, f(RED, 2)); +} + +fn towers(p: &mut Pad) { + // a dark tower with a horned roof and a glowing window + p.poly(&[(9.0, 30.0), (11.5, 12.0), (20.5, 12.0), (23.0, 30.0)], a(IRON)); + for y in [16, 21, 26] { + p.line(11.0, y as f32 + 0.5, 21.5, y as f32 + 0.5, 0.8, f(IRON, 0)); + } + p.rect(8, 9, 23, 12, a(PURPLE)); + for x in [8, 13, 18, 22] { + p.rect(x, 7, x + 1, 8, a(PURPLE)); + } + p.poly(&[(9.0, 8.0), (16.0, 0.5), (23.0, 8.0)], a(IRON)); + p.path(&[(8.5, 10.0), (5.0, 7.0), (4.0, 2.0)], 1.6, a(BONE)); + p.path(&[(23.5, 10.0), (27.0, 7.0), (28.0, 2.0)], 1.6, a(BONE)); + p.disc(16.0, 18.0, 2.2, f(FIRE, 1)); + p.rect(14, 18, 18, 22, f(FIRE, 1)); + p.rect(15, 17, 17, 21, f(FIRE, 2)); + p.px(16, 18, f(FIRE, 3)); + p.rect(14, 27, 18, 30, f(IRON, 0)); + p.disc(16.0, 27.0, 2.4, f(IRON, 0)); + p.rect(3, 30, 28, 30, a(STONE)); +} + +fn drowned(p: &mut Pad) { + // a helmet sinking between waves + p.poly(&[(17.0, 2.0), (22.0, 4.0), (20.0, 6.5), (16.5, 6.0)], a(RED)); + p.disc(15.5, 14.0, 8.0, a(STEEL)); + p.rect(9, 12, 21, 13, f(IRON, 0)); + p.line(15.5, 6.0, 15.5, 20.0, 1.0, f(STEEL, 0)); + p.fill(a(WATER), |x, y| y >= 18.0 + 1.6 * (x * 0.55).sin() && y < 31.0 && (1.0..31.0).contains(&x)); + for (x0, y0) in [(4.0, 23.0), (14.0, 25.0), (22.0, 22.0), (9.0, 28.0), (20.0, 28.5)] { + p.line(x0, y0, x0 + 3.5, y0, 1.0, f(WATER, 3)); + } + p.ring(25.0, 12.0, 1.0, 2.0, f(GLASS, 3)); + p.ring(27.0, 6.0, 0.6, 1.5, f(GLASS, 3)); + p.px(23, 2, f(GLASS, 3)); +} + +fn burnt(p: &mut Pad) { + // a burning bridge plank + p.poly(&[(2.0, 23.0), (28.0, 16.5), (30.0, 22.0), (4.0, 28.5)], a(WOODM)); + p.line(5.0, 25.5, 28.0, 19.5, 0.8, f(WOODM, 0)); + p.poly(&[(19.0, 18.8), (28.0, 16.5), (30.0, 22.0), (21.0, 24.3)], a(IRON)); + p.rect(6, 23, 7, 24, f(IRON, 2)); + p.rect(13, 21, 14, 22, f(IRON, 2)); + for (x, y) in [(20, 20), (23, 21), (25, 19), (27, 20)] { + p.px(x, y, f(FIRE, 1)); + } + // flames + p.poly(&[(14.0, 21.0), (13.0, 14.0), (16.0, 9.0), (17.0, 13.0), (20.0, 4.0), (22.0, 11.0), (24.5, 8.0), (25.5, 14.0), (28.0, 17.0)], f(FIRE, 0)); + p.poly(&[(16.0, 20.0), (16.0, 15.0), (18.0, 12.5), (19.0, 15.0), (20.5, 9.0), (22.0, 14.0), (24.0, 12.0), (24.5, 16.0), (26.0, 18.0)], f(FIRE, 1)); + p.poly(&[(18.5, 19.0), (19.0, 16.0), (20.5, 13.5), (21.5, 16.0), (23.0, 15.5), (23.5, 18.0)], f(FIRE, 2)); + p.px(21, 17, f(FIRE, 3)); + for (x, y) in [(11, 6), (14, 3), (26, 4), (29, 10), (8, 12)] { + p.px(x, y, f(EMBER, 1)); + } +} + +fn siege(p: &mut Pad) { + // a catapult with a boulder + p.line(13.0, 24.0, 17.0, 13.0, 2.0, a(DKWOOD)); + p.line(21.0, 24.0, 17.0, 13.0, 2.0, a(DKWOOD)); + p.line(6.0, 22.0, 24.0, 7.0, 2.2, a(WOODM)); + p.oval(24.5, 6.5, 3.2, 2.0, -0.7, a(DKWOOD)); + p.disc(25.0, 3.5, 2.8, a(STONE)); + p.rect(3, 22, 28, 24, a(WOODM)); + p.rect(3, 23, 28, 23, f(WOODM, 0)); + p.disc(17.0, 13.0, 1.4, a(IRON)); + p.line(6.0, 22.0, 10.0, 22.0, 1.0, f(IRON, 1)); // the rope + for cx in [8.0, 23.0] { + p.disc(cx, 25.5, 4.0, a(DKWOOD)); + p.ring(cx, 25.5, 2.6, 3.3, f(DKWOOD, 0)); + p.disc(cx, 25.5, 1.2, a(STEEL)); + } + p.rect(1, 30, 30, 30, a(STONE)); +} + +fn captured(p: &mut Pad) { + // two iron shackles joined by a chain + p.ring(9.0, 20.0, 3.5, 6.5, a(STEEL)); + p.ring(23.0, 20.0, 3.5, 6.5, a(STEEL)); + p.rect(7, 13, 11, 15, a(IRON)); + p.rect(21, 13, 25, 15, a(IRON)); + p.disc(9.0, 24.5, 1.2, f(IRON, 0)); + p.disc(23.0, 24.5, 1.2, f(IRON, 0)); + // the chain, links turned alternately + let links = [(9.0, 11.0, true), (12.0, 7.5, false), (16.0, 6.0, true), (20.0, 7.5, false), (23.0, 11.0, true)]; + for (x, y, upright) in links { + if upright { + p.oval(x, y, 1.6, 2.6, 0.0, a(STEEL)); + p.rect(x as i32, y as i32 - 1, x as i32, y as i32, f(IRON, 0)); + } else { + p.oval(x, y, 2.6, 1.6, 0.0, a(IRON)); + p.rect(x as i32 - 1, y as i32, x as i32, y as i32, f(IRON, 0)); + } + } +} + +fn drawbridge(p: &mut Pad) { + // a gatehouse with its bridge hanging on chains above the moat + p.rect(14, 4, 29, 29, a(STONE)); + for x in [14, 19, 24] { + p.rect(x, 1, x + 2, 3, a(STONE)); + } + for y in [9, 15, 21] { + p.rect(14, y, 29, y, f(STONE, 0)); + } + p.disc(22.0, 16.0, 4.5, f(IRON, 0)); + p.rect(18, 16, 26, 28, f(IRON, 0)); + for x in [19, 21, 23, 25] { + p.rect(x, 13, x, 22, f(IRON, 2)); + } + p.fill(a(WATER), |x, y| y >= 26.5 + 0.8 * (x * 1.1).sin() && y < 31.0 && (1.0..18.0).contains(&x)); + // the bridge, half raised + p.line(18.0, 27.0, 4.0, 17.0, 4.0, a(WOODM)); + for k in 1..4 { + let t = k as f32 / 4.0; + let (x, y) = (18.0 - 14.0 * t, 27.0 - 10.0 * t); + p.line(x - 1.2, y - 1.7, x + 1.2, y + 1.7, 0.8, f(WOODM, 0)); + } + p.disc(18.0, 27.0, 1.3, a(IRON)); + for k in 0..6 { + let t = k as f32 / 5.0; + let (x, y) = (4.5 + 9.5 * t, 15.0 - 7.0 * t); + p.disc(x, y, 1.1, if k % 2 == 0 { a(STEEL) } else { a(IRON) }); + } +} + +fn wins(p: &mut Pad) { + // a banner planted in the dungeon heart + p.disc(10.5, 20.0, 6.0, a(RED)); + p.disc(21.5, 20.0, 6.0, a(RED)); + p.poly(&[(4.7, 21.5), (27.3, 21.5), (16.0, 31.0)], a(RED)); + p.oval(9.0, 18.5, 2.0, 1.4, -0.6, f(RED, 3)); + p.line(12.0, 2.0, 12.0, 21.0, 1.8, a(DKWOOD)); + p.disc(12.0, 2.0, 1.3, a(GOLDM)); + p.poly(&[(13.0, 3.5), (27.0, 4.5), (23.5, 8.0), (27.0, 11.5), (13.0, 12.5)], a(GOLDM)); + p.line(14.5, 6.0, 21.5, 6.5, 0.8, f(GOLDM, 0)); + p.line(14.5, 9.5, 21.5, 9.5, 0.8, f(GOLDM, 0)); +} + +fn missions(p: &mut Pad) { + // a crystal shard on a rock + p.poly(&[(5.0, 30.0), (8.0, 25.0), (14.0, 24.0), (22.0, 25.0), (27.0, 30.0)], a(STONE)); + p.poly(&[(16.0, 1.5), (22.0, 9.0), (19.5, 27.0), (12.5, 27.0), (10.0, 9.0)], f(CRYSTAL, 1)); + p.poly(&[(16.0, 1.5), (10.0, 9.0), (12.5, 27.0), (14.5, 27.0), (14.0, 9.0)], f(CRYSTAL, 2)); + p.poly(&[(16.0, 1.5), (22.0, 9.0), (19.5, 27.0), (18.5, 27.0), (18.5, 9.0)], f(CRYSTAL, 0)); + p.line(12.0, 6.5, 13.0, 18.0, 0.9, f(CRYSTAL, 3)); + p.poly(&[(6.0, 14.0), (9.5, 18.0), (10.5, 27.0), (6.5, 27.0), (4.5, 19.0)], f(CRYSTAL, 1)); + p.poly(&[(6.0, 14.0), (4.5, 19.0), (6.5, 27.0), (7.5, 27.0), (6.5, 18.0)], f(CRYSTAL, 2)); + p.poly(&[(26.0, 16.0), (28.0, 21.0), (26.5, 27.0), (22.0, 27.0), (23.0, 20.0)], f(CRYSTAL, 0)); + p.sparkle(24, 5); + p.sparkle(4, 8); +} + +fn coop(p: &mut Pad) { + // two claws clasped: an imp's and a demon's + p.line(0.0, 25.0, 11.0, 19.5, 6.0, a(SKIN)); + p.line(32.0, 25.0, 21.0, 19.5, 6.0, a(RED)); + p.line(3.0, 21.0, 6.0, 27.5, 2.0, a(PURPLE)); // cuffs + p.line(29.0, 21.0, 26.0, 27.5, 2.0, a(IRON)); + p.oval(19.5, 18.5, 5.5, 4.5, 0.0, a(RED)); + p.oval(12.5, 18.5, 5.0, 4.8, 0.0, a(SKIN)); + // the imp's fingers curl over the back of the demon's hand + for k in 0..4 { + let x = 15.0 + k as f32 * 2.4; + p.path(&[(x - 0.5, 15.0), (x + 1.0, 17.5), (x + 0.6, 21.0)], 2.0, a(SKIN)); + p.px((x + 0.6) as i32, 22, f(BONE, 3)); + } + // the demon's thumb over the imp's hand + p.line(19.0, 14.5, 12.0, 14.0, 2.2, a(RED)); + p.px(10, 13, f(BONE, 3)); + p.px(11, 13, f(BONE, 2)); + p.sparkle(16, 6); + p.sparkle(8, 9); + p.sparkle(25, 10); +} + +fn hourglass(p: &mut Pad, cx: f32, top: i32, bottom: i32, half: f32, frame: u8) { + let (t, b) = (top as f32, bottom as f32); + let mid = (t + b) * 0.5 + 0.5; + p.poly(&[(cx - half + 1.0, t + 2.0), (cx + half - 1.0, t + 2.0), (cx + 1.0, mid), (cx + half - 1.0, b - 1.0), (cx - half + 1.0, b - 1.0), (cx - 1.0, mid)], f(GLASS, 1)); + p.poly(&[(cx - half + 2.0, t + 2.5), (cx - half + 3.0, t + 2.5), (cx - 0.5, mid - 1.0)], f(GLASS, 3)); + // sand: a little left above, a heap below, the stream + p.poly(&[(cx - half * 0.45, mid - 4.0), (cx + half * 0.45, mid - 4.0), (cx, mid - 0.5)], f(SUN, 1)); + p.poly(&[(cx - half + 1.5, b - 1.0), (cx, b - 6.0), (cx + half - 1.5, b - 1.0)], f(SUN, 1)); + p.poly(&[(cx - 2.0, b - 3.5), (cx, b - 6.0), (cx + 0.5, b - 3.5)], f(SUN, 2)); + p.line(cx, mid, cx, b - 5.0, 0.9, f(SUN, 2)); + p.rect((cx - half) as i32, top, (cx + half) as i32 - 1, top + 1, a(frame)); + p.rect((cx - half) as i32, bottom, (cx + half) as i32 - 1, bottom + 1, a(frame)); + p.line(cx - half + 0.5, t + 1.0, cx - half + 0.5, b, 1.3, a(frame)); + p.line(cx + half - 0.5, t + 1.0, cx + half - 0.5, b, 1.3, a(frame)); +} + +fn minutes(p: &mut Pad) { + // an hourglass with a candle beside it + hourglass(p, 13.0, 3, 27, 8.0, WOODM); + p.rect(23, 15, 27, 28, a(BONE)); + p.rect(22, 28, 28, 29, a(IRON)); + p.line(24.0, 17.0, 24.0, 21.0, 0.9, f(BONE, 3)); + p.px(25, 14, f(IRON, 0)); + p.poly(&[(23.5, 13.0), (25.0, 7.0), (26.6, 13.0)], f(FIRE, 1)); + p.poly(&[(24.4, 13.0), (25.0, 9.5), (25.8, 13.0)], f(FIRE, 3)); +} + +fn flawless(p: &mut Pad) { + // a gleaming, untouched shield + let rim = [(5.0, 3.0), (27.0, 3.0), (27.0, 15.0), (24.0, 22.0), (16.0, 30.0), (8.0, 22.0), (5.0, 15.0)]; + p.poly(&rim, a(GOLDM)); + let field = [(7.5, 5.5), (24.5, 5.5), (24.5, 15.0), (22.0, 21.0), (16.0, 27.0), (10.0, 21.0), (7.5, 15.0)]; + p.poly(&field, a(BLUE)); + p.poly(&[(7.5, 5.5), (16.0, 5.5), (16.0, 27.0), (10.0, 21.0), (7.5, 15.0)], f(BLUE, 2)); + p.rect(15, 7, 16, 23, a(GOLDM)); + p.rect(10, 12, 21, 13, a(GOLDM)); + p.disc(16.0, 12.5, 2.0, a(CRYSTAL)); + p.line(9.0, 7.0, 12.0, 7.0, 0.9, f(BLUE, 3)); + p.line(9.0, 7.0, 9.0, 10.0, 0.9, f(BLUE, 3)); + p.sparkle(26, 4); + p.sparkle(4, 20); + p.px(28, 9, f(SPARK, 2)); +} + +fn speedrun(p: &mut Pad) { + // a winged hourglass + for side in [-1.0f32, 1.0] { + let x = |dx: f32| 16.0 + side * dx; + p.poly(&[(x(6.0), 10.0), (x(13.0), 4.0), (x(15.5), 6.0), (x(14.5), 10.0), (x(15.0), 13.0), (x(13.0), 17.0), (x(6.0), 19.0)], a(BONE)); + for (dx, y) in [(10.0, 9.0), (11.0, 12.0), (10.0, 15.0)] { + p.line(x(6.5), y + 1.5, x(dx + 4.0), y, 0.8, f(BONE, 0)); + } + } + hourglass(p, 16.0, 4, 27, 6.0, GOLDM); + p.path(&[(27.0, 20.0), (25.0, 24.0), (28.0, 24.0), (25.5, 29.5)], 1.2, f(SUN, 2)); + p.path(&[(6.0, 20.0), (4.0, 24.0), (7.0, 24.0), (4.5, 29.5)], 1.2, f(SUN, 2)); +} + +fn campaign(p: &mut Pad) { + // the crown with the core made whole at its heart + p.poly(&[(4.0, 22.0), (3.0, 8.0), (10.0, 15.0), (16.0, 5.0), (22.0, 15.0), (29.0, 8.0), (28.0, 22.0)], a(GOLDM)); + p.rect(4, 21, 27, 27, a(GOLDM)); + p.rect(4, 22, 27, 22, f(GOLDM, 0)); + p.rect(4, 26, 27, 26, f(GOLDM, 0)); + for (x, y) in [(3.0, 7.0), (16.0, 3.5), (29.0, 7.0)] { + p.disc(x, y, 1.8, a(GOLDM)); + } + p.disc(8.5, 24.0, 1.3, a(RED)); + p.disc(23.5, 24.0, 1.3, a(RED)); + // the core + p.poly(&[(16.0, 11.0), (20.5, 17.5), (16.0, 26.0), (11.5, 17.5)], f(CRYSTAL, 1)); + p.poly(&[(16.0, 11.0), (11.5, 17.5), (16.0, 26.0), (15.0, 17.5)], f(CRYSTAL, 2)); + p.px(14, 15, f(CRYSTAL, 3)); + p.px(14, 16, f(CRYSTAL, 3)); + p.sparkle(26, 14); + p.sparkle(7, 13); + p.sparkle(16, 0); +} + +fn surface(p: &mut Pad) { + // the sun rising over grassy hills + for k in 0..8 { + let ang = k as f32 * std::f32::consts::PI / 4.0; + p.line(16.0 + 9.0 * ang.cos(), 14.0 + 9.0 * ang.sin(), 16.0 + 13.0 * ang.cos(), 14.0 + 13.0 * ang.sin(), 2.0, f(SUN, 1)); + } + p.disc(16.0, 14.0, 7.0, f(SUN, 2)); + p.disc(14.5, 12.5, 4.0, f(SUN, 3)); + p.fill(a(GREEN), |x, y| y >= 21.0 + 2.5 * (x * 0.22 + 0.4).sin() && y < 31.0 && (1.0..31.0).contains(&x)); + for x in [3, 7, 12, 19, 24, 28] { + let base = 21.0 + 2.5 * ((x as f32 + 0.5) * 0.22 + 0.4).sin(); + p.line(x as f32 + 0.5, base, x as f32 - 0.5, base - 3.0, 1.0, f(GREEN, 2)); + } + p.px(10, 26, f(RED, 2)); + p.px(22, 27, f(SUN, 2)); + p.px(16, 28, f(SPARK, 1)); +} + +fn defeat(p: &mut Pad) { + // a cup of tea, to soothe the defeat + p.oval(16.0, 27.0, 14.0, 2.8, 0.0, a(BONE)); + p.oval(16.0, 26.5, 9.0, 1.5, 0.0, f(BONE, 0)); + p.ring(24.5, 18.5, 2.0, 4.0, a(BONE)); + p.poly(&[(6.0, 13.0), (26.0, 13.0), (25.0, 20.0), (21.0, 26.0), (11.0, 26.0), (7.0, 20.0)], a(BONE)); + p.rect(7, 16, 25, 17, f(BLUE, 2)); + p.px(12, 16, f(BLUE, 0)); + p.px(16, 17, f(BLUE, 0)); + p.px(20, 16, f(BLUE, 0)); + p.oval(16.0, 13.5, 9.5, 1.8, 0.0, f(TEA, 1)); + p.oval(14.0, 13.0, 3.0, 0.6, 0.0, f(TEA, 3)); + // steam + for x0 in [11.0f32, 16.0, 21.0] { + let mut prev = (x0, 10.0); + for k in 1..6 { + let y = 10.0 - k as f32 * 1.5; + let pt = (x0 + (k as f32 * 1.4).sin() * 1.3, y); + p.line(prev.0, prev.1, pt.0, pt.1, 0.9, f(EMBER, 3)); + prev = pt; + } + } +} + +fn legend(p: &mut Pad) { + // a star crowned with laurels + for side in [-1.0f32, 1.0] { + let mut prev = (16.0 + side * 3.0, 29.0); + for k in 0..7 { + let ang = std::f32::consts::FRAC_PI_2 + side * (0.35 + k as f32 * 0.33); + let pt = (16.0 + 12.0 * ang.cos(), 16.0 + 12.0 * ang.sin()); + p.line(prev.0, prev.1, pt.0, pt.1, 1.0, f(GREEN, 0)); + let out = (16.0 + 14.2 * ang.cos(), 16.0 + 14.2 * ang.sin()); + let inn = (16.0 + 10.0 * ang.cos(), 16.0 + 10.0 * ang.sin()); + p.oval(out.0, out.1, 2.2, 1.1, ang + side * 0.6, a(GREEN)); + if k < 6 { + p.oval(inn.0, inn.1, 2.0, 1.0, ang - side * 0.6, a(GREEN)); + } + prev = pt; + } + } + p.line(13.0, 29.0, 19.0, 29.0, 1.4, a(RED)); + p.star(16.0, 15.5, 4.2, 10.0, a(GOLDM)); + p.star(16.0, 15.5, 1.8, 4.0, f(GOLDM, 3)); +} + +fn wyrm(p: &mut Pad) { + // the ember wyrm's head + p.path(&[(10.0, 9.0), (6.0, 5.0), (2.0, 4.0)], 2.2, a(BONE)); + p.path(&[(13.0, 8.0), (11.0, 3.0), (7.0, 0.5)], 2.0, a(BONE)); + // neck and spines + p.poly(&[(2.0, 31.0), (4.0, 18.0), (9.0, 10.0), (14.0, 22.0), (12.0, 31.0)], a(RED)); + for (x, y) in [(3.0, 22.0), (3.5, 27.0), (5.5, 16.0)] { + p.poly(&[(x, y), (x - 3.0, y + 0.5), (x + 0.5, y + 3.0)], a(FIRE)); + } + // the head, snout to the right, jaw open + p.poly(&[(6.0, 12.0), (12.0, 7.0), (21.0, 9.0), (29.0, 12.0), (29.5, 15.0), (19.0, 16.5), (12.0, 20.0)], a(RED)); + p.poly(&[(12.0, 19.0), (19.0, 18.0), (27.0, 21.0), (26.0, 23.5), (16.0, 24.0), (10.0, 22.0)], a(RED)); + p.poly(&[(19.0, 16.4), (29.0, 15.0), (27.0, 20.5), (19.0, 18.0)], f(IRON, 0)); + for x in [21, 24, 27] { + p.px(x, 16, f(BONE, 3)); + p.px(x - 1, 19, f(BONE, 3)); + } + p.oval(16.0, 11.5, 2.4, 1.4, 0.2, f(SUN, 2)); + p.rect(16, 11, 16, 12, f(IRON, 0)); + p.line(13.0, 9.6, 19.0, 10.0, 0.9, f(RED, 0)); + p.px(27, 11, f(IRON, 0)); + p.line(16.0, 22.0, 25.0, 21.5, 0.8, f(RED, 3)); + // embers from the jaws + for (x, y) in [(30, 18), (28, 25), (30, 27), (24, 27), (29, 22)] { + p.px(x, y, f(EMBER, 1)); + } +} + +fn fullhand(p: &mut Pad) { + // the keeper's hand, full of creatures + // horns and ears behind the heads + p.poly(&[(12.5, 7.5), (11.0, 2.5), (14.5, 6.0)], a(BONE)); + p.poly(&[(19.5, 7.5), (21.0, 2.5), (17.5, 6.0)], a(BONE)); + p.poly(&[(6.0, 14.0), (3.5, 10.0), (8.0, 12.5)], a(GREEN)); + p.poly(&[(26.0, 14.0), (28.5, 10.0), (24.0, 12.5)], a(PURPLE)); + let heads: [(f32, f32, u8); 6] = [(16.0, 10.0, RED), (9.0, 16.5, GREEN), (23.0, 16.5, PURPLE), (16.0, 16.0, BONE), (10.5, 22.0, STEEL), (21.5, 22.0, BRICK)]; + for (x, y, m) in heads { + p.disc(x, y, 4.3, f(IRON, 0)); + p.disc(x, y, 3.6, a(m)); + let (ex, ey) = (x as i32, y as i32); + p.px(ex - 2, ey, f(SUN, 2)); + p.px(ex + 1, ey, f(SUN, 2)); + p.px(ex - 1, ey + 2, f(IRON, 0)); + p.px(ex, ey + 2, f(IRON, 0)); + } + // the cupped hand below + p.poly(&[(2.0, 22.0), (6.0, 25.0), (26.0, 25.0), (30.0, 21.0), (29.0, 27.0), (22.0, 30.5), (9.0, 30.5), (3.0, 27.0)], a(SKIN)); + p.line(30.0, 21.0, 29.0, 15.0, 3.0, a(SKIN)); + p.px(28, 13, f(BONE, 3)); + p.line(2.0, 22.0, 3.0, 17.0, 3.0, a(SKIN)); + p.px(3, 15, f(BONE, 3)); + p.line(8.0, 27.5, 24.0, 27.5, 0.8, f(SKIN, 0)); +} + +// every badge in order of its index +pub fn gen_badges() -> Image { + let rows = (COUNT + COLS - 1) / COLS; + let mut img = Image::gen_image_color((COLS * SIZE) as u16, (rows * SIZE) as u16, Color::new(0.0, 0.0, 0.0, 0.0)); + let painters: [fn(&mut Pad); COUNT as usize] = [ + heroes, bosses, dug, gold, wood, walls, rooms, spells, slaps, traps, towers, drowned, burnt, siege, captured, drawbridge, wins, missions, coop, minutes, flawless, speedrun, campaign, surface, defeat, legend, wyrm, fullhand, + ]; + for (i, paint) in painters.iter().enumerate() { + let mut p = Pad::new(); + paint(&mut p); + p.blit(&mut img, i as u32); + } + img +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_badge_is_drawn() { + let img = gen_badges(); + for i in 0..COUNT { + let r = badge_rect(i); + let mut opaque = 0; + for y in 0..SIZE { + for x in 0..SIZE { + if img.get_pixel(r.x as u32 + x, r.y as u32 + y).a > 0.5 { + opaque += 1; + } + } + } + assert!(opaque > 150, "badge {} has only {} pixels", i, opaque); + } + } + + #[test] + #[ignore] + fn export_badges_png() { + // look at the art: cargo test export_badges_png -- --ignored + let img = gen_badges(); + let mut big = Image::gen_image_color(img.width * 4, img.height * 4, Color::new(0.1, 0.07, 0.07, 1.0)); + for y in 0..img.height as u32 * 4 { + for x in 0..img.width as u32 * 4 { + let c = img.get_pixel(x / 4, img.height as u32 - 1 - y / 4); // export_png flips + if c.a > 0.0 { + big.set_pixel(x, y, c); + } + } + } + big.export_png("/tmp/badges.png"); + } +} diff --git a/src/gfx.rs b/src/gfx.rs index 4c31118..e4eb79c 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -98,6 +98,7 @@ pub struct Gfx { pub atlas: Texture2D, pub sprites: Texture2D, pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*) + pub badges: Texture2D, // achievement badges, 32x32 each (see badges.rs) pub font: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE pub font_map: FontMap, // which glyphs the two fonts hold } @@ -2089,7 +2090,9 @@ impl Gfx { font.set_filter(FilterMode::Nearest); let cursors = Texture2D::from_image(&gen_cursors()); cursors.set_filter(FilterMode::Nearest); - Gfx { atlas, sprites, cursors, font, font_map } + let badges = Texture2D::from_image(&crate::badges::gen_badges()); + badges.set_filter(FilterMode::Nearest); + Gfx { atlas, sprites, cursors, badges, font, font_map } } pub fn cell(id: u32) -> Rect { diff --git a/src/main.rs b/src/main.rs index f3ae377..0205acf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod achievements; mod ai; mod armory; mod audio; +mod badges; mod campaign; mod campaign_doc; mod cloud; diff --git a/src/menu/achievements.rs b/src/menu/achievements.rs index 828588b..d622ac0 100644 --- a/src/menu/achievements.rs +++ b/src/menu/achievements.rs @@ -38,7 +38,7 @@ impl Menu { 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.atlas, ix + 4.0, ry + 4.0, tint, DrawTextureParams { source: Some(crate::gfx::Gfx::cell(*icon)), dest_size: Some(vec2(pic - 8.0, pic - 8.0)), ..Default::default() }); + 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 });