Give every achievement its own pixel-art badge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-06 10:20:41 +01:00
1 parent 368a338ccf
commit fdce6c282b
5 files changed
+976 -33

No files matched your search

+41 -31
View File
@@ -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", "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] = [ pub const TIERS: [(&str, &[f64], u32); 20] = [
("heroes", &[1.0, 50.0, 250.0, 1000.0], crate::gfx::I_ARROW), ("heroes", &[1.0, 50.0, 250.0, 1000.0], crate::badges::HEROES),
("bosses", &[1.0, 5.0, 20.0], crate::gfx::I_RUNE), ("bosses", &[1.0, 5.0, 20.0], crate::badges::BOSSES),
("dug", &[100.0, 1000.0, 10000.0], crate::gfx::I_PICK), ("dug", &[100.0, 1000.0, 10000.0], crate::badges::DUG),
("gold", &[10000.0, 100000.0, 1000000.0], crate::gfx::I_SELL), ("gold", &[10000.0, 100000.0, 1000000.0], crate::badges::GOLD),
("wood", &[50.0, 500.0, 2000.0], crate::gfx::I_BRIDGE), ("wood", &[50.0, 500.0, 2000.0], crate::badges::WOOD),
("walls", &[10.0, 100.0], crate::gfx::I_WALL), ("walls", &[10.0, 100.0], crate::badges::WALLS),
("rooms", &[50.0, 500.0, 2000.0], crate::gfx::I_STEEL), ("rooms", &[50.0, 500.0, 2000.0], crate::badges::ROOMS),
("spells", &[10.0, 100.0, 500.0], crate::gfx::I_BOLT), ("spells", &[10.0, 100.0, 500.0], crate::badges::SPELLS),
("slaps", &[10.0, 100.0, 1000.0], crate::gfx::I_HAND), ("slaps", &[10.0, 100.0, 1000.0], crate::badges::SLAPS),
("traps", &[5.0, 50.0, 200.0], crate::gfx::I_TRAPS), ("traps", &[5.0, 50.0, 200.0], crate::badges::TRAPS),
("towers", &[5.0, 50.0], crate::gfx::I_CANNON), ("towers", &[5.0, 50.0], crate::badges::TOWERS),
("drowned", &[1.0, 25.0, 100.0], crate::gfx::I_FREEZE), ("drowned", &[1.0, 25.0, 100.0], crate::badges::DROWNED),
("burnt", &[5.0, 50.0], crate::gfx::I_DRAWBRIDGE), ("burnt", &[5.0, 50.0], crate::badges::BURNT),
("siege", &[1.0, 10.0], crate::gfx::I_CATAPULT), ("siege", &[1.0, 10.0], crate::badges::SIEGE),
("captured", &[1.0, 25.0, 100.0], crate::gfx::I_SUMMON), ("captured", &[1.0, 25.0, 100.0], crate::badges::CAPTURED),
("drawbridge", &[1.0, 50.0], crate::gfx::I_DRAWBRIDGE), ("drawbridge", &[1.0, 50.0], crate::badges::DRAWBRIDGE),
("wins", &[1.0, 10.0, 50.0], crate::gfx::I_FLAG), ("wins", &[1.0, 10.0, 50.0], crate::badges::WINS),
("missions", &[1.0, 10.0, 30.0], crate::gfx::I_MAGIC), ("missions", &[1.0, 10.0, 30.0], crate::badges::MISSIONS),
("coop", &[1.0, 10.0, 20.0], crate::gfx::I_HEAL), ("coop", &[1.0, 10.0, 20.0], crate::badges::COOP),
("minutes", &[60.0, 600.0, 3000.0], crate::gfx::I_SPELLS), ("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] = [ pub const SPECIAL: [(&str, bool, u32); 8] = [
("flawless", false, crate::gfx::I_HEAL), ("flawless", false, crate::badges::FLAWLESS),
("speedrun", false, crate::gfx::I_BOLT), ("speedrun", false, crate::badges::SPEEDRUN),
("campaign", false, crate::gfx::I_RUNE), ("campaign", false, crate::badges::CAMPAIGN),
("surface", false, crate::gfx::I_WALL), ("surface", false, crate::badges::SURFACE),
("defeat", true, crate::gfx::I_SELL), ("defeat", true, crate::badges::DEFEAT),
("legend", false, crate::gfx::I_ARROW), ("legend", false, crate::badges::LEGEND),
("wyrm", true, crate::gfx::I_SUMMON), ("wyrm", true, crate::badges::WYRM),
("fullhand", true, crate::gfx::I_HAND), ("fullhand", true, crate::badges::FULLHAND),
]; ];
// number of achievements in all // number of achievements in all
@@ -244,7 +244,7 @@ pub fn fmt_count(v: f64) -> String {
out 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)> { pub fn all() -> Vec<(String, bool, u32, usize)> {
let mut v = Vec::new(); let mut v = Vec::new();
for (id, steps, icon) in TIERS.iter() { for (id, steps, icon) in TIERS.iter() {
@@ -268,6 +268,16 @@ mod tests {
assert_eq!(all().len(), count()); 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] #[test]
fn the_tracker_counts_a_game_and_unlocks() { fn the_tracker_counts_a_game_and_unlocks() {
let mut g = Game::new(3, 64, 64); let mut g = Game::new(3, 64, 64);
+929
View File
@@ -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");
}
}
+4 -1
View File
@@ -98,6 +98,7 @@ pub struct Gfx {
pub atlas: Texture2D, pub atlas: Texture2D,
pub sprites: Texture2D, pub sprites: Texture2D,
pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*) 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: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE
pub font_map: FontMap, // which glyphs the two fonts hold pub font_map: FontMap, // which glyphs the two fonts hold
} }
@@ -2089,7 +2090,9 @@ impl Gfx {
font.set_filter(FilterMode::Nearest); font.set_filter(FilterMode::Nearest);
let cursors = Texture2D::from_image(&gen_cursors()); let cursors = Texture2D::from_image(&gen_cursors());
cursors.set_filter(FilterMode::Nearest); 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 { pub fn cell(id: u32) -> Rect {
+1
View File
@@ -4,6 +4,7 @@ mod achievements;
mod ai; mod ai;
mod armory; mod armory;
mod audio; mod audio;
mod badges;
mod campaign; mod campaign;
mod campaign_doc; mod campaign_doc;
mod cloud; mod cloud;
+1 -1
View File
@@ -38,7 +38,7 @@ impl Menu {
draw_rectangle(ix, ry, pic, pic, Color::new(0.08, 0.05, 0.05, 1.0)); 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); 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) }; 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 tx = ix + pic + CW();
let (name, desc) = if *hidden && !got { ("???".to_string(), tr("ach.hidden")) } else { texts(id) }; 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 }); gfx.text(&name, tx, ry + CH() * 0.1, S(), if got { GOLD } else { TXT });