// Procedurally generated pixel art: tile atlas, unit sprites and the bitmap font. use crate::game::EK; use crate::map::{Kind, Owner}; use macroquad::prelude::*; pub const CELL: f32 = 16.0; const ATLAS_COLS: u32 = 16; pub const ATLAS_ROWS: u32 = 14; // atlas indices (4 variants each) pub const T_ROCK: u32 = 0; pub const T_EARTH: u32 = 4; pub const T_GOLD: u32 = 8; pub const T_FLOOR: u32 = 12; pub const T_CLAIM: u32 = 16; pub const T_HEROFLOOR: u32 = 20; pub const T_TREASURY: u32 = 24; pub const T_LAIR: u32 = 28; pub const T_FARM: u32 = 32; pub const T_TRAINING: u32 = 36; pub const T_LIBRARY: u32 = 40; pub const T_COREFLOOR: u32 = 44; pub const T_PORTALFLOOR: u32 = 48; pub const T_GATEFLOOR: u32 = 52; // single cells pub const T_FACE_EARTH: u32 = 56; pub const T_FACE_ROCK: u32 = 57; pub const T_FACE_GOLD: u32 = 58; pub const T_MARK: u32 = 59; pub const D_BED: u32 = 60; pub const D_MUSHROOM: u32 = 61; pub const D_DUMMY: u32 = 62; pub const D_COINS: u32 = 63; pub const D_SHELF: u32 = 64; pub const I_PICK: u32 = 65; pub const I_HAND: u32 = 66; pub const I_SELL: u32 = 67; pub const I_FLAG: u32 = 68; pub const I_BOLT: u32 = 69; pub const I_HEAL: u32 = 70; pub const I_SUMMON: u32 = 71; pub const T_REINF: u32 = 72; // 4 variants: reinforced wall masonry pub const T_TORTURE: u32 = 76; // 4 variants: torture chamber floor pub const D_RACK: u32 = 80; pub const I_ARROW: u32 = 81; pub const I_CANNON: u32 = 82; pub const I_MAGIC: u32 = 83; pub const T_TEMPLE: u32 = 84; // 4 variants pub const D_ALTAR: u32 = 88; pub const T_WATER: u32 = 96; // 4 variants pub const T_LAVA: u32 = 100; // 4 variants pub const T_BRIDGE: u32 = 104; // 2 variants: wooden planks pub const T_STONEBRIDGE: u32 = 106; // 2 variants: stone slabs (over lava) pub const T_FORTWALL: u32 = 108; // 4 variants: knight fortress masonry pub const D_SHARD: u32 = 112; // shard of the soul core pub const D_BANNER: u32 = 113; // knight banner pub const I_BRIDGE: u32 = 114; pub const T_SILVER: u32 = 115; // 2 variants each: veins in the earth pub const T_RUBY: u32 = 117; pub const T_DIAMOND: u32 = 119; pub const D_SILVER: u32 = 121; // treasury content pub const D_RUBY: u32 = 122; pub const D_DIAMOND: u32 = 123; pub const D_RUBBLE: u32 = 124; // floor decals: pebbles and fragments pub const D_CRACK: u32 = 125; // a long crack through several stones pub const T_AERIE: u32 = 126; // 4 variants: straw, twigs and feathers pub const T_STEEL: u32 = 130; // 4 variants: riveted iron plates pub const I_STEEL: u32 = 134; pub const T_RIVAL1: u32 = 135; // 4 variants: floor of the green master pub const T_RIVAL2: u32 = 139; // 4 variants: floor of the blue master pub const I_RUNE: u32 = 143; // rune forge tower pub const T_FORGE: u32 = 144; // 4 variants: sooty flagstones with glowing cracks pub const I_TRAPS: u32 = 148; // 10 icons in TRAP_KINDS order pub const I_SPELLS: u32 = 158; // 8 icons in SPELLS order pub const T_PRISON: u32 = 166; // 4 variants each pub const T_GRAVEYARD: u32 = 170; pub const T_GUARDPOST: u32 = 174; pub const T_ARENA: u32 = 178; pub const T_ALTAR: u32 = 182; pub const I_ORDERS: u32 = 186; // attack, guard pub const T_RIVAL3: u32 = 189; // 4 variants: floor of the yellow master pub struct Gfx { pub atlas: Texture2D, pub sprites: Texture2D, pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*) 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 } // ------------------------------------------------------------------ bitmap font (Dinowyde) // // assets/dinowyde_1x.png: 6x8 px glyphs, ordered by Unicode in five columns of 16 glyphs // (96 px) and 48 rows each: // column 0: U+0000-02FF (Basic Latin, Latin-1, Latin Extended A/B, IPA, spacing modifiers) // column 1: U+0310-05FF (combining marks from U+0310, Greek, Cyrillic, Armenian, Hebrew; // U+0300-030F have no slot) // column 2: U+0600-06FF (Arabic) in rows 0-15, U+2000-21FF (punctuation ... arrows) in rows 16-47 // column 3: U+2200-24FF (mathematical operators, technical, ...) // column 4: U+2500-27FF (box drawing, blocks, geometric shapes, ...) pub const GW: f32 = 6.0; // glyph width pub const GH: f32 = 8.0; // glyph height const FONT_COLS: usize = 80; // glyphs per row of the sheet const FONT_ROWS: usize = 48; // place of a character in the sheet (glyph index row * FONT_COLS + column), if the sheet has a slot for it pub fn font_slot(ch: char) -> Option { let u = ch as usize; let (block, off) = match u { 0x0000..=0x02FF => (0, u), 0x0310..=0x05FF => (1, u - 0x0310), 0x0600..=0x06FF => (2, u - 0x0600), 0x2000..=0x21FF => (2, 0x100 + u - 0x2000), 0x2200..=0x24FF => (3, u - 0x2200), 0x2500..=0x27FF => (4, u - 0x2500), _ => return None, }; Some((off / 16) * FONT_COLS + block * 16 + off % 16) } // Characters Dinowyde does not draw yet (arrows, card suits, smileys, notes, ...) come from the // DINOBYTE font (assets/dbyte_1x.png, CP437 order, 16x16 glyphs), which sits right of the // Dinowyde sheet in the font texture. const DBYTE_X: usize = FONT_COLS * GW as usize; // x of the DINOBYTE sheet in the font texture const CP437_LOW: &str = " ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼"; const CP437_HIGH: &str = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}"; // the CP437 code of a character (DINOBYTE), if it has one fn cp437(ch: char) -> Option { // DINOBYTE has ö (0x94) and ò (0x95) swapped match ch { 'ö' => return Some(0x95), 'ò' => return Some(0x94), '⌂' => return Some(127), _ => {} } let u = ch as u32; if (32..127).contains(&u) { return Some(u as u8); } if let Some(i) = CP437_LOW.chars().position(|c| c == ch).filter(|&i| i > 0) { return Some(i as u8); } CP437_HIGH.chars().position(|c| c == ch).map(|i| 128 + i as u8) } // stand-ins for characters neither font draws: similar glyphs fn font_substitute(ch: char) -> Option { Some(match ch { '✓' | '✔' => '√', 'ẞ' => 'ß', '–' | '—' => '-', '„' | '“' | '”' => '"', '‚' | '‘' | '’' => '\'', '…' => '.', _ => return None, }) } // which glyphs the two sheets hold pub struct FontMap { has: Vec, // Dinowyde slots with a glyph dbyte: [bool; 256], // DINOBYTE codes with a glyph } fn glyph_drawn(data: &[[u8; 4]], w: usize, gx: usize, gy: usize) -> bool { (0..GH as usize).any(|y| (0..GW as usize).any(|x| data.get((gy + y) * w + gx + x).map_or(false, |p| p[3] > 0))) } // the font texture: Dinowyde, and DINOBYTE right of it; white glyphs on a transparent // background (for tinting) pub fn load_font() -> (Image, FontMap) { let wyde = Image::from_file_with_format(include_bytes!("../assets/dinowyde_1x.png"), Some(ImageFormat::Png)).expect("font"); let byte = Image::from_file_with_format(include_bytes!("../assets/dbyte_1x.png"), Some(ImageFormat::Png)).expect("font"); let (w, h) = (DBYTE_X + byte.width as usize, (wyde.height as usize).max(byte.height as usize)); let mut img = Image::gen_image_color(w as u16, h as u16, Color::new(0.0, 0.0, 0.0, 0.0)); { let out = img.get_image_data_mut(); let ww = wyde.width as usize; for (i, p) in wyde.get_image_data().iter().enumerate() { if p[3] > 128 { out[(i / ww) * w + i % ww] = [255, 255, 255, 255]; } } let bw = byte.width as usize; for (i, p) in byte.get_image_data().iter().enumerate() { if p[3] > 128 && p[0] > 128 { out[(i / bw) * w + DBYTE_X + i % bw] = [255, 255, 255, 255]; } } } let data = img.get_image_data(); let has = (0..FONT_COLS * FONT_ROWS).map(|g| glyph_drawn(data, w, (g % FONT_COLS) * GW as usize, (g / FONT_COLS) * GH as usize)).collect(); let mut dbyte = [false; 256]; for (c, d) in dbyte.iter_mut().enumerate() { *d = glyph_drawn(data, w, DBYTE_X + (c % 16) * GW as usize, (c / 16) * GH as usize); } (img, FontMap { has, dbyte }) } // where in the font texture a character's glyph is (top-left pixel): Dinowyde, else // DINOBYTE, else a stand-in, else '?' pub fn font_src(map: &FontMap, ch: char) -> (f32, f32) { let find = |c: char| -> Option<(f32, f32)> { if let Some(g) = font_slot(c).filter(|&g| map.has.get(g).copied().unwrap_or(false)) { return Some(((g % FONT_COLS) as f32 * GW, (g / FONT_COLS) as f32 * GH)); } cp437(c).filter(|&k| map.dbyte[k as usize]).map(|k| ((DBYTE_X + (k as usize % 16) * GW as usize) as f32, (k / 16) as f32 * GH)) }; find(ch).or_else(|| font_substitute(ch).and_then(find)).or_else(|| find('?')).unwrap_or((0.0, 0.0)) } // frame glyphs: [top-left, top-right, bottom-left, bottom-right, horizontal, vertical] pub const BOX_SINGLE: [char; 6] = ['┌', '┐', '└', '┘', '─', '│']; pub const BOX_DOUBLE: [char; 6] = ['╔', '╗', '╚', '╝', '═', '║']; fn hash(x: i32, y: i32, s: u32) -> u32 { let mut h = (x as u32).wrapping_mul(374761393) ^ (y as u32).wrapping_mul(668265263) ^ s.wrapping_mul(2246822519); h = (h ^ (h >> 13)).wrapping_mul(1274126177); h ^ (h >> 16) } fn rnd(x: i32, y: i32, s: u32) -> f32 { (hash(x, y, s) & 0xffff) as f32 / 65535.0 } fn c(r: i32, g: i32, b: i32) -> Color { Color::from_rgba(r.clamp(0, 255) as u8, g.clamp(0, 255) as u8, b.clamp(0, 255) as u8, 255) } fn shade(base: (i32, i32, i32), d: i32) -> Color { c(base.0 + d, base.1 + d, base.2 + d) } struct Canvas { img: Image, } impl Canvas { fn cell(&mut self, id: u32, f: impl Fn(i32, i32) -> Option) { let ox = (id % ATLAS_COLS) * 16; let oy = (id / ATLAS_COLS) * 16; for y in 0..16 { for x in 0..16 { if let Some(col) = f(x, y) { self.img.set_pixel(ox + x as u32, oy + y as u32, col); } } } } } // flagstone pattern with grout; stones are whole, cracked or broken (chipped corner with grit) fn stones(x: i32, y: i32, v: u32, base: (i32, i32, i32), grout: (i32, i32, i32), var: i32) -> Color { let row = y / 8; let off = if row % 2 == 1 { 4 } else { 0 }; let sx = (x + off) % 16; if y % 8 == 0 || sx % 8 == 0 { return shade(grout, (rnd(x, y, v + 7) * 6.0) as i32); } let (lx, ly) = (sx % 8, y % 8); // position inside the stone (1..7) let damage = hash((x + off) / 8, row, v + 17) % 9; match damage { // a crack running across the stone 0 | 1 => { let k = (hash((x + off) / 8, row, v + 18) % 3) as i32; let on = if damage == 0 { lx + k == ly || lx + k == ly + 1 && ly % 3 == 0 } else { 8 - lx == ly + k - 1 }; if on { return shade(grout, -6); } } // broken corner: the piece is missing, grit lies in the hole 2 => { if lx + ly < 5 { return if hash(x, y, v + 19) % 3 == 0 { shade(base, -18) } else { shade(grout, (rnd(x, y, v + 20) * 10.0) as i32 - 4) }; } } // shattered: several small fragments 3 => { if (lx * 3 + ly * 5 + v as i32) % 7 == 0 || lx == ly / 2 + 2 { return shade(grout, 2); } } _ => {} } let stone = hash((x + off) / 8, row, v + 3) % 5; let d = (rnd(x, y, v) * var as f32) as i32 - var / 2 + stone as i32 * 3 - 6; // highlight top/left, shadow bottom/right let edge = if y % 8 == 1 || sx % 8 == 1 { 10 } else if y % 8 == 7 || sx % 8 == 7 { -8 } else { 0 }; shade(base, d + edge) } fn earth_px(x: i32, y: i32, v: u32) -> Color { let base = (98, 68, 44); let big = rnd(x / 3, y / 3, v + 11) * 16.0 - 8.0; let fine = rnd(x, y, v) * 14.0 - 7.0; if hash(x, y, v + 99) % 37 == 0 { return c(150, 120, 90); } if hash(x, y, v + 98) % 29 == 0 { return c(62, 42, 28); } shade(base, (big + fine) as i32) } fn gen_atlas() -> Image { let mut cv = Canvas { img: Image::gen_image_color((ATLAS_COLS * 16) as u16, (ATLAS_ROWS * 16) as u16, Color::new(0.0, 0.0, 0.0, 0.0)) }; for v in 0..4u32 { // rock cv.cell(T_ROCK + v, |x, y| { let blk = rnd(x / 4 + (y / 4) % 2, y / 4, v + 21) * 20.0 - 10.0; let fine = rnd(x, y, v + 5) * 10.0 - 5.0; let crack = (x + y * 3 + v as i32 * 5) % 23 == 0 || (x * 2 - y + v as i32) % 31 == 0; if crack { Some(c(40, 40, 48)) } else { Some(shade((80, 80, 92), (blk + fine) as i32)) } }); // earth cv.cell(T_EARTH + v, |x, y| Some(earth_px(x, y, v))); // gold cv.cell(T_GOLD + v, |x, y| { let mut best = 99.0f32; for k in 0..6 { let nx = 2 + (hash(k, 1, v + 40) % 12) as i32; let ny = 2 + (hash(k, 2, v + 40) % 12) as i32; let d = (((x - nx) * (x - nx) + (y - ny) * (y - ny)) as f32).sqrt(); best = best.min(d - if k % 2 == 0 { 0.5 } else { 0.0 }); } if best < 0.6 { Some(c(255, 240, 150)) } else if best < 1.4 { Some(c(240, 196, 52)) } else if best < 2.0 { Some(c(168, 118, 30)) } else { Some(earth_px(x, y, v + 60)) } }); cv.cell(T_FLOOR + v, |x, y| Some(stones(x, y, v, (64, 56, 50), (34, 30, 28), 10))); cv.cell(T_CLAIM + v, |x, y| { let col = stones(x, y, v + 10, (78, 44, 58), (36, 20, 28), 10); // small emblem in the middle of every other stone if (x % 8 == 4 && y % 8 == 4) && hash(x / 8, y / 8, v) % 2 == 0 { Some(c(150, 60, 80)) } else { Some(col) } }); cv.cell(T_HEROFLOOR + v, |x, y| Some(stones(x, y, v + 20, (132, 136, 150), (80, 84, 96), 12))); cv.cell(T_TREASURY + v, |x, y| { let chk = ((x / 4) + (y / 4)) % 2 == 0; let base = if chk { (120, 92, 36) } else { (100, 74, 30) }; let d = (rnd(x, y, v + 30) * 10.0) as i32 - 5; if x % 4 == 0 || y % 4 == 0 { Some(shade((70, 52, 22), d)) } else { Some(shade(base, d)) } }); cv.cell(T_LAIR + v, |x, y| { let streak = ((x * 2 + y + (hash(y, 0, v) % 3) as i32) % 5) as i32; let d = (rnd(x, y, v + 31) * 18.0) as i32 - 9 + streak * 4; Some(shade((112, 86, 46), d)) }); cv.cell(T_FARM + v, |x, y| { let moss = rnd(x / 2, y / 2, v + 32); let d = (rnd(x, y, v + 33) * 14.0) as i32 - 7; if moss > 0.55 { Some(shade((58, 98, 40), d)) } else { Some(shade((62, 46, 32), d)) } }); cv.cell(T_TRAINING + v, |x, y| { let plank = y / 4; let off = (hash(plank, 0, v + 34) % 16) as i32; if y % 4 == 0 || (x + off) % 16 == 0 { Some(c(58, 38, 24)) } else { let grain = ((x + plank * 5) % 7 == 0) as i32 * -10; Some(shade((116, 80, 48), (rnd(x, y, v + 35) * 10.0) as i32 + grain)) } }); cv.cell(T_LIBRARY + v, |x, y| { if x >= 3 && x <= 12 && y >= 3 && y <= 12 { let border = x == 3 || x == 12 || y == 3 || y == 12; if border { Some(c(170, 130, 50)) } else { Some(shade((116, 30, 42), (rnd(x, y, v + 36) * 10.0) as i32)) } } else { let plank = x / 4; if x % 4 == 0 { Some(c(40, 24, 16)) } else { Some(shade((74, 46, 32), (rnd(x, y, v + 37) * 10.0) as i32 + (plank % 2) * 4)) } } }); cv.cell(T_COREFLOOR + v, |x, y| Some(stones(x, y, v + 40, (86, 32, 34), (40, 14, 16), 10))); cv.cell(T_PORTALFLOOR + v, |x, y| Some(stones(x, y, v + 50, (66, 44, 92), (32, 20, 46), 10))); cv.cell(T_GATEFLOOR + v, |x, y| { let vein = ((x as f32 * 0.7 + (y as f32 * 0.5).sin() * 3.0 + v as f32 * 4.0) as i32) % 9 == 0; if vein { Some(c(150, 150, 165)) } else { Some(shade((196, 196, 206), (rnd(x, y, v + 60) * 12.0) as i32 - 6)) } }); } // wall faces cv.cell(T_FACE_EARTH, |x, y| { let streak = (hash(x, 0, 70) % 3) as i32 * 5; Some(shade((62, 42, 28), (rnd(x, y, 71) * 10.0) as i32 - streak - y / 2)) }); cv.cell(T_FACE_ROCK, |x, y| { let streak = (hash(x, 0, 72) % 3) as i32 * 5; Some(shade((54, 54, 64), (rnd(x, y, 73) * 10.0) as i32 - streak - y / 2)) }); cv.cell(T_FACE_GOLD, |x, y| { if hash(x, y, 74) % 9 == 0 { Some(c(200, 160, 40)) } else { Some(shade((62, 42, 28), (rnd(x, y, 75) * 10.0) as i32 - y / 2)) } }); // dig marker: diagonal stripes cv.cell(T_MARK, |x, y| { if (x + y) % 6 < 2 { Some(Color::from_rgba(255, 220, 60, 150)) } else if x == 0 || y == 0 || x == 15 || y == 15 { Some(Color::from_rgba(255, 220, 60, 200)) } else { None } }); // bed cv.cell(D_BED, |x, y| { let dx = (x as f32 - 7.5) / 6.5; let dy = (y as f32 - 8.0) / 5.5; let d = dx * dx + dy * dy; if d < 0.55 { Some(if x < 5 { c(200, 190, 160) } else { c(110, 50, 60) }) } else if d < 1.0 { Some(c(150, 118, 64)) } else { None } }); // mushrooms cv.cell(D_MUSHROOM, |x, y| { let caps = [(4, 6, 3), (11, 5, 2), (8, 11, 3)]; for &(cx, cy, r) in &caps { let dx = x - cx; let dy = y - cy; if dy <= 0 && dx * dx + dy * dy * 2 <= r * r + 1 { return Some(if (x + y) % 3 == 0 && dy < 0 { c(250, 230, 220) } else { c(200, 40, 50) }); } if dx.abs() <= 0 && dy > 0 && dy <= r { return Some(c(230, 220, 200)); } } None }); // training dummy cv.cell(D_DUMMY, |x, y| { if (x == 7 || x == 8) && y >= 4 { Some(c(90, 60, 36)) } else if y == 7 && (3..=12).contains(&x) { Some(c(90, 60, 36)) } else if (x - 7).pow(2) + (y - 3).pow(2) <= 5 { Some(c(200, 170, 110)) } else if y >= 14 && (4..=11).contains(&x) { Some(c(60, 40, 24)) } else { None } }); // gold pile cv.cell(D_COINS, |x, y| { let dx = (x as f32 - 7.5) / 7.0; let dy = (y as f32 - 11.0) / 5.0; let d = dx * dx + dy * dy; if d < 1.0 && y <= 14 { let hl = hash(x, y, 80) % 5 == 0; Some(if hl { c(255, 245, 170) } else if d > 0.6 { c(190, 140, 30) } else { c(242, 200, 60) }) } else { None } }); // bookshelf cv.cell(D_SHELF, |x, y| { if y > 9 || x == 0 || x == 15 { return if y <= 10 { Some(c(60, 36, 22)) } else { None }; } if y == 0 || y == 5 { return Some(c(60, 36, 22)); } let book = hash(x, y / 5, 81) % 5; Some(match book { 0 => c(150, 40, 40), 1 => c(40, 80, 140), 2 => c(60, 120, 60), 3 => c(160, 130, 50), _ => c(100, 60, 120), }) }); // reinforced walls: masonry in dark stone with the player's red mortar for v in 0..4u32 { cv.cell(T_REINF + v, |x, y| { let row = y / 4; let off = if row % 2 == 1 { 4 } else { 0 }; if y % 4 == 3 || (x + off) % 8 == 7 { return Some(shade((70, 26, 30), (rnd(x, y, v + 90) * 8.0) as i32)); } let brick = hash((x + off) / 8, row, v + 91) % 4; Some(shade((84, 76, 74), (rnd(x, y, v + 92) * 14.0) as i32 - 7 + brick as i32 * 5 - 6 + if y % 4 == 0 { 10 } else { 0 })) }); // torture chamber: dark stones with rusty iron grates cv.cell(T_TORTURE + v, |x, y| { let grate = (4..=11).contains(&x) && (4..=11).contains(&y) && (x % 3 == 1 || y % 3 == 1); if grate { return Some(shade((96, 44, 30), (rnd(x, y, v + 93) * 12.0) as i32)); } Some(stones(x, y, v + 94, (52, 40, 44), (26, 16, 18), 10)) }); } // temple: dark marble with a glowing red-gold rune circle for v in 0..4u32 { cv.cell(T_TEMPLE + v, |x, y| { let (dx, dy) = (x as f32 - 7.5, y as f32 - 7.5); let d = (dx * dx + dy * dy).sqrt(); if (5.0..6.0).contains(&d) { return Some(c(170, 70, 40)); } if d < 1.5 { return Some(c(220, 150, 60)); } let vein = ((x as f32 * 0.6 + (y as f32 * 0.7).sin() * 2.0 + v as f32 * 3.0) as i32) % 7 == 0; Some(if vein { c(70, 54, 66) } else { shade((44, 34, 46), (rnd(x, y, v + 95) * 10.0) as i32) }) }); } // altar with a flame cv.cell(D_ALTAR, |x, y| { if y >= 9 && (3..=12).contains(&x) { Some(if y == 9 { c(150, 140, 150) } else { c(100, 92, 104) }) } else if y < 9 && y >= 3 && (x - 7).abs() + (y - 8).abs() / 2 <= 3 { Some(if (x + y) % 3 == 0 { c(255, 220, 80) } else { c(255, 120, 30) }) } else { None } }); // torture wheel cv.cell(D_RACK, |x, y| { let (dx, dy) = (x as f32 - 7.5, y as f32 - 7.0); let d = (dx * dx + dy * dy).sqrt(); let a = dy.atan2(dx); if (5.5..7.0).contains(&d) || (d < 6.0 && (a * 4.0 / std::f32::consts::PI).fract().abs() < 0.12) { Some(c(110, 76, 44)) } else if y >= 13 && (x == 7 || x == 8 || (4..=11).contains(&x) && y == 15) { Some(c(70, 48, 30)) } else { None } }); // tool icons cv.cell(I_PICK, |x, y| { let handle = (x + y == 16 || x + y == 17) && x >= 3; let head = (y == 3 || y == 4) && (2..=13).contains(&x) && (x - 8).abs() + (y - 2) * 2 < 9; if head { Some(c(200, 205, 215)) } else if handle { Some(c(140, 90, 50)) } else { None } }); cv.cell(I_HAND, |x, y| { let palm = (3..=12).contains(&x) && (7..=14).contains(&y); let fingers = y >= 2 && y < 8 && (x == 4 || x == 6 || x == 8 || x == 10) && !(x == 10 && y < 4); let thumb = (x == 13 || x == 14) && (6..=10).contains(&y); if palm || fingers || thumb { Some(c(220, 180, 150)) } else { None } }); cv.cell(I_SELL, |x, y| { let d = (x - 8) * (x - 8) + (y - 8) * (y - 8); if d <= 36 && d >= 20 { Some(c(240, 200, 60)) } else if (x == 8 && (4..=12).contains(&y)) || (y == 8 && (5..=11).contains(&x)) { Some(c(240, 200, 60)) } else { None } }); cv.cell(I_FLAG, |x, y| { if x == 3 && y >= 1 { Some(c(180, 180, 180)) } else if x > 3 && x < 14 && y >= 2 && y < 9 - ((x - 4) / 3) % 2 { Some(c(210, 40, 40)) } else { None } }); cv.cell(I_BOLT, |x, y| { let line1 = y < 8 && (x - (10 - y / 2)).abs() <= 1; let line2 = y >= 7 && y <= 9 && (5..=11).contains(&x); let line3 = y > 8 && (x - (8 - (y - 9) / 2)).abs() <= 1; if line1 || line2 || line3 { Some(c(255, 240, 120)) } else { None } }); cv.cell(I_HEAL, |x, y| { if ((6..=9).contains(&x) && (2..=13).contains(&y)) || ((2..=13).contains(&x) && (6..=9).contains(&y)) { Some(c(90, 230, 110)) } else { None } }); cv.cell(I_ARROW, |x, y| { // a small wooden tower with an arrow if (4..=11).contains(&x) && y >= 7 { Some(if (x + y) % 4 == 0 { c(90, 60, 34) } else { c(130, 88, 50) }) } else if y == 6 && x % 2 == 0 && (4..=11).contains(&x) { Some(c(130, 88, 50)) } else if y == 3 && (2..=13).contains(&x) || (x == 12 && (y == 2 || y == 4)) { Some(c(220, 220, 220)) } else { None } }); cv.cell(I_CANNON, |x, y| { let barrel = y >= 4 && y <= 7 && x >= 5 && x <= 14 && (y - 4) as i32 <= 3; let wheel = (x - 6) * (x - 6) + (y - 10) * (y - 10) <= 9; if barrel { Some(c(50, 50, 56)) } else if wheel { Some(c(120, 80, 40)) } else { None } }); cv.cell(I_MAGIC, |x, y| { let crystal = (x - 8).abs() + (y - 5).abs() <= 3; if crystal { Some(c(190, 110, 255)) } else if (6..=9).contains(&x) && y >= 9 { Some(c(110, 110, 124)) } else { None } }); cv.cell(I_RUNE, |x, y| { // a stone obelisk with a glowing rune, bronze bands let body = (5..=10).contains(&x) && y >= 3 && y <= 14 && !(y == 3 && (x == 5 || x == 10)); if !body { return None; } let rune = (x == 7 || x == 8) && (6..=10).contains(&y) || (y == 8 && (6..=9).contains(&x)); Some(if rune { c(90, 210, 255) } else if y == 4 || y == 12 { c(190, 120, 60) } else if (x + y) % 5 == 0 { c(80, 80, 92) } else { c(110, 110, 124) }) }); cv.cell(I_SUMMON, |x, y| { let d = (((x - 8) * (x - 8) + (y - 8) * (y - 8)) as f32).sqrt(); let a = ((y - 8) as f32).atan2((x - 8) as f32); if d < 7.0 && ((a * 3.0 + d * 0.9).sin() > 0.3) { Some(c(200, 90, 230)) } else { None } }); // ---- campaign terrain for v in 0..4u32 { // water: dark blue with lighter wave crests. Seamless (period 16): the shader lets it flow // across the tiles, sampled in world space cv.cell(T_WATER + v, |x, y| { let tau = std::f32::consts::TAU; let bend = ((y as f32 + v as f32 * 3.0) * tau / 16.0).sin() * 1.6 + ((x as f32) * tau / 8.0).sin() * 0.6; let wave = (x + y * 2 + bend.round() as i32).rem_euclid(8); let d = (rnd(x, y, v + 120) * 10.0) as i32 - 5; Some(match wave { 0 => shade((70, 120, 170), d), 1 => shade((50, 92, 140), d), _ => shade((24, 50, 88), d), }) }); // lava: molten orange with hot yellow streaks and darker red troughs, no black crust. // Seamless (period 16) so it can flow across the tiles cv.cell(T_LAVA + v, |x, y| { let tau = std::f32::consts::TAU; let (fx, fy) = (x as f32 / 16.0, y as f32 / 16.0); let o = v as f32 * 1.3; let heat = (tau * (fx + fy * 2.0) + o).sin() * 0.45 + (tau * (fx * 3.0 - fy) + (tau * fy * 2.0 + o).sin() * 1.2).sin() * 0.35 + (tau * (fy * 3.0 + fx * 2.0) + o * 2.0).sin() * 0.2; let d = (rnd(x, y, v + 134) * 16.0) as i32 - 8; Some(if heat > 0.62 { shade((255, 228, 120), d / 2) } else if heat > 0.3 { shade((255, 170, 55), d) } else if heat > -0.35 { shade((240, 110, 25), d) } else if heat > -0.7 { shade((200, 64, 18), d) } else { shade((150, 40, 14), d) }) }); // knight fortress walls: pale stone blocks with blue-grey mortar cv.cell(T_FORTWALL + v, |x, y| { let row = y / 5; let off = if row % 2 == 1 { 5 } else { 0 }; if y % 5 == 4 || (x + off) % 10 == 9 { return Some(shade((70, 76, 92), (rnd(x, y, v + 140) * 8.0) as i32)); } let blk = hash((x + off) / 10, row, v + 141) % 4; Some(shade((150, 150, 160), (rnd(x, y, v + 142) * 12.0) as i32 - 6 + blk as i32 * 5 - 8 + if y % 5 == 0 { 10 } else { 0 })) }); } for v in 0..2u32 { // wooden bridge: planks across, two rope rails at the sides cv.cell(T_BRIDGE + v, |x, y| { if x <= 1 || x >= 14 { return Some(if (y + v as i32) % 4 == 0 { c(150, 120, 80) } else { c(96, 70, 42) }); } if y % 4 == 0 { return Some(c(40, 26, 16)); } let plank = y / 4; let off = (hash(plank, 1, v + 150) % 16) as i32; if (x + off) % 13 == 0 && y % 4 == 2 { return Some(c(60, 40, 24)); // nail } Some(shade((126, 88, 52), (rnd(x, y, v + 151) * 14.0) as i32 - 7 + (plank % 2) * 6)) }); // stone bridge over lava: big grey slabs, sooty edges cv.cell(T_STONEBRIDGE + v, |x, y| { if x <= 1 || x >= 14 { return Some(shade((60, 56, 56), (rnd(x, y, v + 160) * 10.0) as i32)); } Some(stones(x, y, v + 161, (104, 98, 96), (46, 40, 40), 12)) }); } // shard of the soul core: a glowing red crystal cv.cell(D_SHARD, |x, y| { let (dx, dy) = ((x - 8).abs(), y); if y >= 2 && y <= 14 && dx * 3 <= (if y < 7 { (y - 2) * 4 } else { (15 - y) * 2 }) { Some(if dx == 0 || (x + y) % 5 == 0 { c(255, 170, 170) } else if x < 8 { c(230, 50, 60) } else { c(160, 20, 40) }) } else if dy == 15 && (4..=12).contains(&x) { Some(c(80, 30, 40)) } else { None } }); // knight banner: pole with a white-blue banner and a golden sun cv.cell(D_BANNER, |x, y| { if x == 3 { return Some(c(170, 170, 180)); } if (4..=13).contains(&x) && (1..=11).contains(&y) { let (dx, dy) = (x - 9, y - 6); if dx * dx + dy * dy <= 5 { return Some(c(240, 200, 60)); } return Some(if y == 11 && x % 2 == 0 { c(60, 90, 170) } else if x % 5 == 4 { c(200, 205, 220) } else { c(230, 232, 240) }); } None }); // bridge tool icon: an arch with planks cv.cell(I_BRIDGE, |x, y| { let (dx, dy) = (x as f32 - 7.5, y as f32 - 13.0); let d = (dx * dx + dy * dy).sqrt(); if y <= 6 && y >= 5 && (1..=14).contains(&x) { Some(if x % 3 == 0 { c(70, 46, 26) } else { c(150, 106, 60) }) } else if (6.0..7.5).contains(&d) && y >= 6 { Some(c(120, 84, 48)) } else if y > 6 && (x == 1 || x == 14) { Some(c(110, 76, 44)) } else if y >= 12 && d >= 7.5 && (2..=13).contains(&x) { Some(c(60, 110, 170)) } else { None } }); // ---- more treasures: veins (earth with embedded pieces) and treasury piles let vein = |x: i32, y: i32, v: u32, seed: u32, hi: Color, mid: Color, lo: Color, size: f32| -> Color { let mut best = 99.0f32; for k in 0..5 { let nx = 2 + (hash(k, 1, v + seed) % 12) as i32; let ny = 2 + (hash(k, 2, v + seed) % 12) as i32; best = best.min((((x - nx).abs() + (y - ny).abs()) as f32) - if k % 2 == 0 { 0.5 } else { 0.0 }); } if best < size * 0.4 { hi } else if best < size { mid } else if best < size + 0.8 { lo } else { earth_px(x, y, v + seed) } }; for v in 0..2u32 { cv.cell(T_SILVER + v, |x, y| Some(vein(x, y, v, 170, c(245, 245, 255), c(180, 186, 200), c(100, 104, 118), 2.0))); cv.cell(T_RUBY + v, |x, y| Some(vein(x, y, v, 180, c(255, 150, 170), c(210, 30, 60), c(110, 16, 36), 1.6))); cv.cell(T_DIAMOND + v, |x, y| Some(vein(x, y, v, 190, c(255, 255, 255), c(150, 230, 255), c(70, 140, 190), 1.4))); } // treasury heaps, same dome shape as the gold pile: silver bars, rubies, diamonds let heap = |x: i32, y: i32| -> Option { let dx = (x as f32 - 7.5) / 7.0; let dy = (y as f32 - 11.0) / 5.0; let d = dx * dx + dy * dy; if d < 1.0 && y <= 14 { Some(d) } else { None } }; cv.cell(D_SILVER, |x, y| { let d = heap(x, y)?; // bars: 4 px wide bricks in rows of 2 let row = y / 2; let off = if row % 2 == 1 { 2 } else { 0 }; Some(if y % 2 == 0 { c(240, 242, 250) } else if (x + off) % 4 == 0 { c(110, 114, 128) } else if d > 0.6 { c(150, 156, 172) } else { c(196, 200, 216) }) }); let gems = |x: i32, y: i32, seed: u32, hi: Color, mid: Color, lo: Color| -> Option { let d = heap(x, y)?; // facetted stones: diamond shaped cells of 4 px let (u, v) = (x + y, x - y + 16); let (cu, cv) = (u / 4, v / 4); let e = (u % 4 == 0) || (v % 4 == 0); let h = hash(cu, cv, seed) % 3; Some(if e { lo } else if h == 0 || (u % 4 == 1 && v % 4 == 1) { hi } else if d > 0.65 { lo } else { mid }) }; // forge: sooty dark flagstones, iron filings and glowing cracks for v in 0..4u32 { cv.cell(T_FORGE + v, |x, y| { let base = stones(x, y, v + 300, (52, 46, 46), (22, 18, 18), 8); if (x * 7 + y * 3 + v as i32 * 11) % 23 == 0 || ((x + y * 2 + v as i32) % 19 == 0 && y % 8 != 0) { Some(c(255, 120, 40)) } else if hash(x, y, v + 301) % 13 == 0 { Some(c(120, 120, 130)) } else { Some(base) } }); } // prison: dark flagstones with drain grates for v in 0..4u32 { cv.cell(T_PRISON + v, |x, y| { if (5..=10).contains(&x) && (5..=10).contains(&y) && (x == 5 || x == 10 || y == 5 || y == 10 || x % 2 == 0) && v % 2 == 0 { Some(c(30, 30, 34)) } else { Some(stones(x, y, v + 310, (58, 56, 60), (28, 26, 30), 6)) } }); // graveyard: dark earth, moss and pebbles cv.cell(T_GRAVEYARD + v, |x, y| { let r = rnd(x, y, v + 320); Some(if r > 0.86 { c(70, 96, 52) } else if r > 0.8 { c(110, 106, 100) } else { shade((52, 40, 34), (rnd(x / 2, y / 2, v + 321) * 16.0) as i32 - 8) }) }); // guard post: flagstones with a red border cv.cell(T_GUARDPOST + v, |x, y| { if x == 0 || y == 0 || x == 15 || y == 15 { Some(c(120, 30, 34)) } else { Some(stones(x, y, v + 330, (76, 66, 62), (36, 30, 30), 8)) } }); // arena: raked sand with dark stains cv.cell(T_ARENA + v, |x, y| { let rake = (y + (x / 4 + v as i32) % 2) % 3 == 0; let stain = rnd(x / 3, y / 3, v + 340) > 0.9; Some(if stain { c(110, 60, 40) } else if rake { c(170, 140, 96) } else { shade((196, 164, 112), (rnd(x, y, v + 341) * 14.0) as i32 - 7) }) }); // altar: black marble with red veins cv.cell(T_ALTAR + v, |x, y| { let vein = ((x as f32 * 0.7 + (y as f32 * 0.5 + v as f32).sin() * 3.0) as i32 + y) % 11 == 0; Some(if vein { c(150, 20, 30) } else { shade((30, 26, 32), (rnd(x, y, v + 350) * 10.0) as i32 - 5) }) }); } // trap and door icons let icons: [fn(i32, i32) -> Option; 10] = [ // spikes |x, y| { if y >= 12 { Some(c(90, 90, 100)) } else if y >= 3 && (x % 4 - 2).abs() <= (y - 3) / 4 && (2..14).contains(&x) { Some(if x % 4 == 2 { c(230, 230, 240) } else { c(160, 160, 175) }) } else { None } }, // poison cloud |x, y| { let d = |cx: i32, cy: i32, r: i32| (x - cx).pow(2) + (y - cy).pow(2) <= r * r; if d(5, 9, 4) || d(10, 8, 4) || d(8, 5, 3) { Some(if (x + y) % 5 == 0 { c(200, 255, 140) } else { c(110, 200, 70) }) } else { None } }, // fire jet |x, y| { let w = (y - 1).max(0) / 3; if (x - 8).abs() <= w && y <= 14 && y >= 1 { Some(if (x - 8).abs() <= w / 2 && y > 6 { c(255, 230, 120) } else { c(255, 120, 30) }) } else { None } }, // stone roller |x, y| { let d = (x - 8).pow(2) + (y - 8).pow(2); if d <= 36 { Some(if (x + y) % 6 == 0 || d > 28 { c(80, 76, 72) } else { c(140, 132, 124) }) } else if y == 14 && x < 15 { Some(c(90, 70, 50)) } else { None } }, // alarm bell |x, y| { let w = 2 + y / 3; if (2..=11).contains(&y) && (x - 8).abs() <= w.min(6) { Some(if x < 8 { c(255, 220, 90) } else { c(200, 160, 50) }) } else if (12..=13).contains(&y) && (x - 8).abs() <= 1 { Some(c(140, 100, 40)) } else { None } }, // rune mine |x, y| { let d = (((x - 8).pow(2) + (y - 8).pow(2)) as f32).sqrt(); if (5.0..6.5).contains(&d) || ((x - 8).abs() <= 1 && (3..=12).contains(&y)) || (y == 8 && (5..=11).contains(&x)) { Some(c(90, 200, 255)) } else { None } }, // frost glow |x, y| { let (dx, dy) = (x - 8, y - 8); if (dx == 0 || dy == 0 || dx == dy || dx == -dy) && dx.abs() <= 6 && dy.abs() <= 6 { Some(if dx.abs() + dy.abs() <= 2 { c(255, 255, 255) } else { c(150, 220, 255) }) } else { None } }, // wooden door |x, y| { if (3..=12).contains(&x) && y >= 2 { Some(if x % 3 == 0 { c(80, 50, 28) } else if y == 5 || y == 11 { c(70, 70, 76) } else { c(130, 86, 48) }) } else { None } }, // iron door |x, y| { if (3..=12).contains(&x) && y >= 2 { Some(if (x == 4 || x == 11) && y % 3 == 0 { c(200, 200, 210) } else if y == 7 { c(60, 60, 66) } else { c(110, 112, 122) }) } else { None } }, // magic door |x, y| { if (3..=12).contains(&x) && y >= 2 { let rune = (x == 8 && (5..=11).contains(&y)) || (y == 8 && (6..=10).contains(&x)); Some(if rune { c(240, 160, 255) } else if x == 3 || x == 12 || y == 2 { c(70, 40, 100) } else { c(110, 60, 160) }) } else { None } }, ]; for (k, f) in icons.iter().enumerate() { cv.cell(I_TRAPS + k as u32, |x, y| f(x, y)); } // spell icons let spells: [fn(i32, i32) -> Option; 8] = [ // seer's eye |x, y| { let (dx, dy) = ((x - 8) as f32, (y - 8) as f32 * 1.8); let d = (dx * dx + dy * dy).sqrt(); if d < 2.5 { Some(c(40, 10, 60)) } else if d < 4.5 { Some(c(200, 120, 255)) } else if d < 7.0 { Some(c(235, 225, 245)) } else { None } }, // cave-in |x, y| { if y >= 9 && (x + y * 3) % 5 != 0 { Some(c(110, 80, 56)) } else if y < 9 && ((x * 7 + y * 5) % 9 == 0) { Some(c(140, 110, 80)) } else if y < 3 { Some(c(70, 60, 56)) } else { None } }, // haste |x, y| { let bolt = (y < 8 && (x - (12 - y / 2)).abs() <= 1) || (y >= 8 && (x - (10 - (y - 8) / 2) + 4).abs() <= 1) || (y == 8 && (5..=11).contains(&x)); if bolt { Some(c(255, 230, 90)) } else if (y == 4 || y == 11) && x < 4 { Some(c(255, 200, 80)) } else { None } }, // stoneskin |x, y| { if (3..=12).contains(&x) && (2..=13).contains(&y) && !(y > 10 && (x - 8).abs() + (y - 10) > 5) { Some(if (x + y) % 4 == 0 { c(110, 110, 104) } else { c(170, 168, 160) }) } else { None } }, // gold rain |x, y| { if y % 4 == (x / 3) % 4 && x % 3 != 2 { Some(c(255, 210, 60)) } else { None } }, // invisibility |x, y| { let d = (x - 8).pow(2) + (y - 7).pow(2); if (d <= 20 && (x + y) % 2 == 0) || (y >= 11 && (x - 8).abs() <= 4 && (x + y) % 2 == 0) { Some(c(200, 220, 255)) } else { None } }, // plague |x, y| { let d = |cx: i32, cy: i32, r: i32| (x - cx).pow(2) + (y - cy).pow(2) <= r * r; if d(8, 8, 5) && !d(8, 8, 2) { Some(c(140, 210, 60)) } else if d(8, 8, 1) || d(3, 4, 1) || d(13, 12, 1) { Some(c(90, 150, 40)) } else { None } }, // quake |x, y| { if y >= 7 && y <= 9 && (x + (y - 8)) % 4 != 0 { Some(c(140, 110, 80)) } else if (x - 8 + (y - 8).abs() * 0).abs() <= 0 && y > 9 { Some(c(60, 40, 30)) } else if y > 9 && (x + y) % 3 == 0 { Some(c(100, 76, 56)) } else if y < 6 && (x * 5 + y) % 7 == 0 { Some(c(170, 140, 100)) } else { None } }, ]; for (k, f) in spells.iter().enumerate() { cv.cell(I_SPELLS + k as u32, |x, y| f(x, y)); } // order icons: crossed blades (attack), a shield (guard) cv.cell(I_ORDERS, |x, y| { let blade = (x - y).abs() <= 1 || (x + y - 15).abs() <= 1; if blade && (2..=13).contains(&x) && (2..=13).contains(&y) { Some(if (x - y).abs() == 0 || (x + y - 15) == 0 { c(240, 240, 250) } else { c(170, 170, 185) }) } else if (y == 12 || y == 13) && (x == 2 || x == 3 || x == 12 || x == 13) { Some(c(140, 90, 40)) } else { None } }); cv.cell(I_ORDERS + 1, |x, y| { let w = if y < 9 { 6 } else { 6 - (y - 8) }; if (2..=14).contains(&y) && (x - 8).abs() <= w.max(0) { Some(if (x - 8).abs() == w.max(0) || y == 2 { c(220, 190, 80) } else if x == 8 || y == 6 { c(220, 190, 80) } else { c(150, 30, 36) }) } else { None } }); // aerie: straw and twigs on stone, a few feathers for v in 0..4u32 { cv.cell(T_AERIE + v, |x, y| { let base = stones(x, y, v + 230, (70, 62, 58), (36, 30, 30), 8); let twig = (x * 3 + y * 7 + v as i32 * 5) % 13 == 0 || (x * 5 - y * 2 + v as i32) % 17 == 0; let straw = rnd(x / 2, y, v + 231) > 0.62; if hash(x, y, v + 232) % 41 == 0 { Some(c(235, 232, 225)) // feather } else if twig { Some(c(96, 66, 40)) } else if straw { Some(shade((190, 160, 80), (rnd(x, y, v + 233) * 20.0) as i32 - 10)) } else { Some(base) } }); } // floors of the rival masters: like the player's claimed floor, in their colours for v in 0..4u32 { for (id, base, grout, mark) in [(T_RIVAL1, (44, 74, 50), (20, 36, 24), (90, 170, 100)), (T_RIVAL2, (44, 54, 86), (20, 24, 44), (90, 120, 200)), (T_RIVAL3, (74, 62, 26), (36, 30, 12), (210, 185, 70))] { cv.cell(id + v, |x, y| { let col = stones(x, y, v + 250, base, grout, 10); if (x % 8 == 4 && y % 8 == 4) && hash(x / 8, y / 8, v + 251) % 2 == 0 { Some(c(mark.0, mark.1, mark.2)) } else { Some(col) } }); } } // steel walls: riveted plates with a little rust for v in 0..4u32 { cv.cell(T_STEEL + v, |x, y| { let seam = x % 8 == 0 || y % 8 == 0; let rivet = (x % 8 == 2 || x % 8 == 6) && (y % 8 == 2 || y % 8 == 6); let rust = rnd(x / 2, y / 2, v + 240) > 0.86; if seam { Some(c(44, 48, 58)) } else if rivet { Some(c(170, 176, 190)) } else if rust { Some(shade((120, 70, 40), (rnd(x, y, v + 241) * 16.0) as i32)) } else { Some(shade((96, 102, 116), (rnd(x, y, v + 242) * 10.0) as i32 - 5 + if y % 8 == 1 { 10 } else { 0 })) } }); } cv.cell(I_STEEL, |x, y| { if (2..=13).contains(&x) && (3..=12).contains(&y) { let rivet = (x == 4 || x == 11) && (y == 5 || y == 10); Some(if rivet { c(220, 224, 235) } else if x == 2 || y == 3 || x == 13 || y == 12 { c(60, 64, 76) } else { c(130, 136, 150) }) } else { None } }); // floor decals cv.cell(D_RUBBLE, |x, y| { for k in 0..9 { let gx = 1 + (hash(k, 5, 220) % 14) as i32; let gy = 1 + (hash(k, 6, 220) % 14) as i32; let s = (hash(k, 7, 220) % 3) as i32; if (x - gx).abs() + (y - gy).abs() <= s { return Some(if y < gy { c(120, 110, 104) } else if (x + y) % 2 == 0 { c(84, 76, 72) } else { c(66, 60, 56) }); } } None }); cv.cell(D_CRACK, |x, y| { let cx = 8.0 + ((y as f32) * 0.7).sin() * 3.0 + (y as f32 * 1.9).sin(); if (x as f32 - cx).abs() < 0.7 || (y > 9 && (x as f32 - cx - (y - 9) as f32 * 0.8).abs() < 0.6) { Some(Color::from_rgba(20, 14, 14, 200)) } else { None } }); cv.cell(D_RUBY, |x, y| gems(x, y, 200, c(255, 150, 170), c(215, 30, 60), c(120, 12, 36))); cv.cell(D_DIAMOND, |x, y| gems(x, y, 210, c(255, 255, 255), c(170, 232, 255), c(80, 150, 200))); cv.img } // ------------------------------------------------------------------ Sprites (10x10) const SPR: usize = 10; const SPR_CELL: u32 = 12; fn sprite_rows(k: EK) -> [&'static str; 10] { match k { EK::Worker => [ "..........", "k..kkkk..k", "okkooookko", ".koeooeok.", "..kooook..", "...kRRk..S", "..kRRRRkSS", "..kRRRRkb.", "...kRRkb..", "...k..k...", ], EK::Kobold => [ "........s.", "..kkkk..s.", ".kggggk.b.", ".kgegegkb.", ".kggggkkb.", "..kGGkggb.", ".kgGGGgkb.", ".kgGGGk.b.", "..kGkGk...", "..kk.kk...", ], EK::Spider => [ "..........", "...kkkk...", "k.kPPPPk.k", ".kPPPPPPk.", "kkPePPePkk", ".kPPPPPPk.", "k.kPPPPk.k", ".k.kkkk.k.", "k........k", "..........", ], EK::Mage => [ "....kk....", "...kPPk...", "..kPPPPk..", ".kkkkkkkk.", "..kfefek.y", "..kPPPPk.b", ".kPpPPpPkb", ".kPPpPPPkb", ".kPPPPPPk.", "..kkkkkk..", ], EK::Ogre => [ "..kkkkkk..", ".kcccccck.", ".kcecceck.", ".kcCccCck.", "kcccccccck", "kcCcccccCk", "kccCccCcck", ".kcccccck.", ".kcCk.kCk.", ".kkk...kk.", ], EK::Knight => [ "...kkkk...", "..kssssk..", "..kSkkSk.s", "..kssssk.s", "..kuuuuk.s", ".ksuuuusks", ".kSuyyuSk.", "..kuuuuk..", "..kSkkSk..", "..kk..kk..", ], EK::Archer => [ "...kkkk..b", "..kGGGGkbk", "..kGfkfkb.", "..kGfffkb.", "..kgggggb.", ".kfgggggb.", "..kggggkbk", "..kBBBBk.b", "..kBkkBk..", "..kk..kk..", ], EK::Priest => [ "....yy....", "...kkkk...", "..kffffk..", "..kfkfkk..", "..kwwwwk..", ".kwwyywwk.", ".kwwyywwk.", ".kwwwwwwk.", ".kwwwwwwk.", "..kkkkkk..", ], EK::Dwarf => [ "..........", "...kkkk...", "..kSSSSk..", "..kSSSSkS.", "..kfkfkkSS", "..kooook.b", ".kboooobkb", ".kbBBBBbk.", "..kBkkBk..", "..kk..kk..", ], EK::Jailer => [ "...kkkk...", "..kCCCCk..", "..kCkkCk..", "..kCeeCk..", ".kCCCCCCk.", "kCCRRRRCCk", "kCCRRRRCCS", ".kCCCCCCkS", "..kCk.kCkS", "..kk...kk.", ], EK::Priestess => [ "....oo..y.", "...kkkk.o.", "..kffffk..", "..kfkfkk..", "..kRRRRk..", ".kRRooRRk.", ".kRRooRRkf", ".kRRRRRRk.", ".kRRRRRRk.", "..kkkkkk..", ], // flying creatures EK::Bat => [ "..........", "k..k..k..k", "Pk.kPPk.kP", "PPkPPPPkPP", "PPPPrPrPPP", "PPPPPPPPPP", ".PPkPPkPP.", "..k.PP.k..", ".....k....", "..........", ], EK::Gargoyle => [ "w........w", "Cw.kkkk.wC", "CCkceeckCC", "CCkcccckCC", "CkcccccckC", "k.kcCCck.k", ".kcccccck.", ".kcCkkCck.", ".kck..kck.", ".kk....kk.", ], EK::Owl => [ "..k....k..", ".kBk..kBk.", ".kbbbbbbk.", "kbeebbeebk", "kbekbbekbk", ".kbbyybbk.", "kubbbbbbuk", "kubbwwbbuk", ".kubbbbuk.", "..ky..yk..", ], // cogling: a sooty little tinkerer with brass goggles and a hammer EK::Cogling => [ "........SS", "...kkkk.SS", "..kOOOOk.b", ".kyekkeykb", "..kOooOk.b", "...kCCk..b", "..kOCCOkbb", "..kOCCOk..", "...kO.Ok..", "...kk.kk..", ], // revenant: a pale, hollow-eyed corpse with a rusty blade and green grave-light EK::Revenant => [ "...kkkk...", "..kwwwwk..", "..kwgwgk..", "..kwwwwk.S", "...kCCk..S", "..kCcCCk.S", ".kCkcCkCkb", "..kCcCCk..", "..kCk.kCk.", "..kk...kk.", ], // giant: a huge brute in a leather jerkin with a tree-trunk club EK::Giant => [ "...kkkk...", "..kffffk..", "..kfefek.b", "..kffffk.b", ".kBBBBBBkb", "kfBuuuuBfb", "kfBuuuuBf.", ".kBBBBBBk.", ".kBBk.kBBk", ".kkk...kkk", ], // fairy: a tiny glowing spy with dragonfly wings EK::Fairy => [ "..........", "ww.....ww.", "www.kk.www", ".wwkffkww.", "..wkefkw..", "...kuuk...", "...kuuk...", "....kk....", "...y..y...", "..........", ], // battle mage: robes of the order, a burning staff EK::BattleMage => [ "....kk...o", "...kuuk.oy", "..kuuuuk.o", ".kkffffk.b", "..kfefek.b", "..kwuuwk.b", ".kuwuuwukb", ".kuuwwuuk.", ".kuuuuuuk.", "..kkkkkk..", ], EK::Wyrm => [ "R.......kw", "RR....krrk", "RRR..krekr", "RRRRkrrrrk", ".RRrrrrok.", "..rrooork.", ".krrooorr.", "krrrrrrRk.", "k.rrk.rk..", "..kk..kk..", ], // obsidian demon with glowing cracks, broken chains and twin axes EK::Chainbreaker => [ "s.kkkkkk.s", "sskdoodkss", ".SkeddekS.", ".SkdoodkS.", "kdddoodddk", "kdoddddodk", "SkddooddkS", ".kddddddk.", ".kdok.odk.", ".kkk..kkk.", ], EK::Paladin => [ "...kkkk..w", "..kyyyyk.w", "..kYkkYk.w", "..kyyyykkw", ".kwwwwwwkk", "kywwyywwyk", ".kwyyyywk.", "..kwwwwk..", "..kYkkYk..", "..kk..kk..", ], } } fn pal(ch: char) -> Option { Some(match ch { 'k' => c(22, 16, 22), 'w' => c(240, 240, 236), 'y' => c(250, 210, 60), 'Y' => c(190, 140, 30), 'r' => c(200, 40, 40), 'R' => c(130, 34, 30), 'g' => c(96, 176, 64), 'G' => c(44, 104, 44), 'b' => c(140, 92, 52), 'B' => c(92, 58, 32), 's' => c(206, 210, 220), 'S' => c(124, 128, 144), 'p' => c(176, 96, 220), 'P' => c(92, 40, 124), 'u' => c(64, 104, 204), 'U' => c(32, 52, 124), 'f' => c(232, 184, 144), 'e' => c(255, 240, 80), 'c' => c(150, 152, 140), 'C' => c(96, 98, 92), 'o' => c(226, 124, 48), 'O' => c(160, 72, 24), 'd' => c(44, 36, 48), _ => return None, }) } pub const SPRITE_KINDS: [EK; 22] = [ EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Paladin, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Bat, EK::Gargoyle, EK::Owl, EK::Wyrm, EK::Cogling, EK::Revenant, EK::Giant, EK::Fairy, EK::BattleMage, ]; // width of the sprite texture in pixels pub const SPRITE_TEX_W: f32 = (SPRITE_KINDS.len() * 12) as f32; pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * SPR_CELL) as f32; pub fn sprite_index(k: EK) -> u32 { SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32 } // Equipment tier of a level: 0 (Lv 1-2) none, 1 (3-4) shoulder pieces, 2 (5-6) + helmet, // 3 (7-8) + cape, 4 (9-10) all in gold with a crown pub const GEAR_TIERS: u32 = 5; pub fn gear_tier(level: u32) -> u32 { ((level.max(1) - 1) / 2).min(GEAR_TIERS - 1) } fn is_hero_kind(k: EK) -> bool { matches!(k, EK::Knight | EK::Archer | EK::Priest | EK::Dwarf | EK::Paladin | EK::Giant | EK::Fairy | EK::BattleMage) } // Views of a sprite for eight directions (the others are mirrored): 0 front, 1 front // three-quarter, 2 side, 3 back three-quarter, 4 back. The drawn sprites face the viewer; // the turned views are derived from them and face to the right. pub const VIEWS: u32 = 5; type Rows = [[u8; SPR]; SPR]; fn is_body(ch: u8) -> bool { !matches!(ch, b'.' | b'k' | b'e' | b'f') } // the commonest body colour of a sprite (for painting over eyes and faces) fn body_char(r: &Rows) -> u8 { let mut n = [0usize; 128]; for row in r.iter() { for &ch in row.iter() { if is_body(ch) && ch < 128 { n[ch as usize] += 1; } } } (0..128u8).max_by_key(|&c| n[c as usize]).filter(|&c| n[c as usize] > 0).unwrap_or(b'k') } // moves the eyes one pixel to the right (where there is face or body to move onto) fn eyes_right(r: &mut Rows) { for row in r.iter_mut() { for x in (0..SPR - 1).rev() { if row[x] == b'e' && row[x + 1] != b'k' && row[x + 1] != b'.' && row[x + 1] != b'e' { row[x] = row[x + 1]; row[x + 1] = b'e'; } } } } // removes the middle column of the silhouette: a narrower body, as seen from the side fn narrow(r: &mut Rows) { let cols: Vec = (0..SPR).filter(|&x| r.iter().any(|row| row[x] != b'.')).collect(); let (Some(&l), Some(&rr)) = (cols.first(), cols.last()) else { return }; let m = (l + rr) / 2; for row in r.iter_mut() { for x in (1..=m).rev() { row[x] = row[x - 1]; } row[0] = b'.'; } } fn sprite_view(k: EK, view: u32) -> Rows { let mut r: Rows = [[b'.'; SPR]; SPR]; for (y, row) in sprite_rows(k).iter().enumerate() { for (x, ch) in row.bytes().enumerate().take(SPR) { r[y][x] = ch; } } let body = body_char(&r); match view { 1 => eyes_right(&mut r), 2 => { narrow(&mut r); // in profile only the eye on the facing side is seen for row in r.iter_mut() { let eyes: Vec = (0..SPR).filter(|&x| row[x] == b'e').collect(); if eyes.len() >= 2 { let x = eyes[0]; row[x] = if x > 0 && is_body(row[x - 1]) { row[x - 1] } else if row[x + 1] == b'f' { b'f' } else { body }; } } eyes_right(&mut r); } 3 | 4 => { // from behind: mirrored (the weapon is in the other hand), no eyes, no face for row in r.iter_mut() { row.reverse(); } for y in 0..SPR { for x in 0..SPR { let ch = r[y][x]; if ch == b'e' || ch == b'f' { let above = if y > 0 { r[y - 1][x] } else { b'.' }; r[y][x] = if is_body(above) { above } else { body }; } } } if view == 3 { narrow(&mut r); } } _ => {} } r } // the 12x12 cell of a kind with its gear painted around and onto the silhouette fn sprite_with_gear(k: EK, tier: u32, view: u32) -> [[Option; 12]; 12] { let mut g = [[None; 12]; 12]; for (y, row) in sprite_view(k, view).iter().enumerate() { for (x, &ch) in row.iter().enumerate() { g[y + 1][x + 1] = pal(ch as char); } } if tier == 0 { return g; } let metal = match tier { 1 => c(196, 126, 60), 4 => c(250, 206, 72), _ => c(208, 214, 228), }; let dark = Color::new(metal.r * 0.6, metal.g * 0.6, metal.b * 0.6, 1.0); let span = |g: &[[Option; 12]; 12], y: usize| -> Option<(usize, usize)> { let xs: Vec = (0..12).filter(|&x| g[y][x].is_some()).collect(); Some((*xs.first()?, *xs.last()?)) }; let top = (0..12).find(|&y| g[y].iter().any(|p| p.is_some())).unwrap_or(1); // cape (behind the figure: only where the sprite is empty) if tier >= 3 { let cape = if is_hero_kind(k) { c(44, 74, 156) } else { c(128, 22, 34) }; for y in (top + 3).min(11)..=(top + 8).min(11) { if let Some((l, r)) = span(&g, y) { for x in [l.wrapping_sub(1), r + 1] { if x < 12 && g[y][x].is_none() { g[y][x] = Some(cape); } } } } } // shoulder pieces at the widest row of the upper body let shoulder = (top + 2..(top + 6).min(11)).max_by_key(|&y| span(&g, y).map_or(0, |(l, r)| r - l)).unwrap_or(top + 3); if let Some((l, r)) = span(&g, shoulder) { for x in [l, r] { g[shoulder][x] = Some(metal); if shoulder + 1 < 12 && g[shoulder + 1][x].is_some() { g[shoulder + 1][x] = Some(dark); } } } // helmet: the top row in metal, horns (creatures) or a plume (heroes) if tier >= 2 { if let Some((l, r)) = span(&g, top) { for x in l..=r { if g[top][x].is_some() { g[top][x] = Some(metal); } } if top >= 1 { if is_hero_kind(k) { g[top - 1][(l + r) / 2] = Some(c(200, 40, 40)); } else { let bone = c(232, 222, 200); if l >= 1 { g[top - 1][l - 1] = Some(bone); g[top][l - 1] = Some(bone); } if r + 1 < 12 { g[top - 1][r + 1] = Some(bone); g[top][r + 1] = Some(bone); } } } } } // crown if tier >= 4 && top >= 1 { if let Some((l, r)) = span(&g, top) { let m = (l + r) / 2; for x in [m.saturating_sub(1), m + 1] { if x < 12 { g[top - 1][x] = Some(c(255, 230, 90)); } } } } g } // row (view * GEAR_TIERS + tier) * 2: sprites with the gear of the tier, the row after it: // white silhouettes (hit flash) fn gen_sprites() -> Image { let n = SPRITE_KINDS.len() as u32; let mut img = Image::gen_image_color((n * SPR_CELL) as u16, (2 * VIEWS * GEAR_TIERS * SPR_CELL) as u16, Color::new(0.0, 0.0, 0.0, 0.0)); for (i, &k) in SPRITE_KINDS.iter().enumerate() { for view in 0..VIEWS { for tier in 0..GEAR_TIERS { let g = sprite_with_gear(k, tier, view); for (y, row) in g.iter().enumerate() { for (x, px) in row.iter().enumerate() { if let Some(col) = px { let (px, py) = (i as u32 * SPR_CELL + x as u32, (view * GEAR_TIERS + tier) * 2 * SPR_CELL + y as u32); img.set_pixel(px, py, *col); img.set_pixel(px, py + SPR_CELL, WHITE); } } } } } } img } // ------------------------------------------------------------------ mouse pointers (16x16) pub const CURSOR_POINT: u32 = 0; // pointing monster hand (hot spot: finger tip) pub const CURSOR_OPEN: u32 = 1; // open grabbing hand (hot spot: palm centre) pub const CURSOR_FIST: u32 = 2; // holding something pub const CURSOR_BACKHAND: u32 = 3; // slap, with motion streaks pub const CURSOR_PUNCH: u32 = 4; // punch, with impact star pub const CURSOR_HOT: [(f32, f32); 5] = [(2.0, 1.0), (7.0, 7.0), (7.0, 7.0), (7.0, 7.0), (7.0, 7.0)]; const CUR_POINT: [&str; 16] = [ "..kk............", ".kwwk...........", ".kggk...........", ".kggk...........", ".kggkkk.........", ".kggkggkkk......", ".kggkggkggkk....", "kkggggggggggk...", "kgkgggggggggk...", "kggggggggggGk...", ".kgggggggggGk...", "..kggggggggGk...", "..kgggggggGk....", "...kGGGGGGk.....", "...kkkkkkkk.....", "................", ]; const CUR_OPEN: [&str; 16] = [ "................", ".kw.kw.kw.......", ".kgkkgkkgk..kw..", ".kgkkgkkgk.kgk..", ".kgkkgkkgkkggk..", ".kgggggggggggk..", "kkgggggggggggk..", "kgkggggggggggk..", "kggkgggggggGk...", ".kggggggggggk...", "..kgggggggGk....", "...kggggggGk....", "...kgggggGk.....", "....kGGGGk......", "....kkkkkk......", "................", ]; const CUR_FIST: [&str; 16] = [ "................", "................", "................", "...kkkkkkkk.....", "..kgkgkgkgGk....", "..kgkgkgkgGk....", ".kkggggggggGk...", "kggkgggggggGk...", "kgggggggggGGk...", ".kggggggggGk....", "..kgggggggGk....", "...kggggggGk....", "...kGGGGGGk.....", "....kkkkkk......", "................", "................", ]; fn cursor_pal(ch: char) -> Option { Some(match ch { 'k' => c(20, 16, 18), 'g' => c(112, 152, 84), 'G' => c(70, 100, 56), 'w' => c(236, 230, 208), 'y' => c(255, 220, 80), 's' => c(255, 255, 255), _ => return None, }) } fn gen_cursors() -> Image { let mut img = Image::gen_image_color(16 * 5, 16, Color::new(0.0, 0.0, 0.0, 0.0)); let mut put = |frame: u32, rows: &[&str; 16], mirror: bool| { for (y, row) in rows.iter().enumerate() { for (x, ch) in row.chars().enumerate() { if let Some(col) = cursor_pal(ch) { let xx = if mirror { 15 - x } else { x }; img.set_pixel(frame * 16 + xx as u32, y as u32, col); } } } }; put(CURSOR_POINT, &CUR_POINT, false); put(CURSOR_OPEN, &CUR_OPEN, false); put(CURSOR_FIST, &CUR_FIST, false); put(CURSOR_BACKHAND, &CUR_OPEN, true); put(CURSOR_PUNCH, &CUR_FIST, false); // motion streaks for the backhand, impact star for the punch for &(x, y) in &[(0, 5), (1, 5), (0, 7), (1, 7), (2, 7), (0, 9), (1, 9)] { img.set_pixel(CURSOR_BACKHAND * 16 + x, y, c(255, 255, 255)); } for &(x, y) in &[(0, 0), (2, 1), (1, 2), (0, 3), (3, 0), (4, 2), (2, 4)] { img.set_pixel(CURSOR_PUNCH * 16 + x, y, c(255, 220, 80)); } img } impl Gfx { pub fn new() -> Gfx { let atlas = Texture2D::from_image(&gen_atlas()); atlas.set_filter(FilterMode::Nearest); let sprites = Texture2D::from_image(&gen_sprites()); sprites.set_filter(FilterMode::Nearest); // the fonts (Dinowyde, DINOBYTE for what it lacks) let (img, font_map) = load_font(); let font = Texture2D::from_image(&img); font.set_filter(FilterMode::Nearest); let cursors = Texture2D::from_image(&gen_cursors()); cursors.set_filter(FilterMode::Nearest); Gfx { atlas, sprites, cursors, font, font_map } } pub fn cell(id: u32) -> Rect { Rect::new((id % ATLAS_COLS) as f32 * CELL, (id / ATLAS_COLS) as f32 * CELL, CELL, CELL) } // the plain 10x10 sprite (interface icons) pub fn sprite_rect(k: EK, white: bool) -> Rect { let i = sprite_index(k); Rect::new((i * SPR_CELL + 1) as f32, (1 + if white { SPR_CELL } else { 0 }) as f32, SPR as f32, SPR as f32) } // the whole 12x12 cell with the gear of a level (billboards) pub fn sprite_cell(k: EK, white: bool, level: u32) -> Rect { Gfx::sprite_cell_view(k, white, level, 0) } // the cell of one of the five views (see VIEWS) pub fn sprite_cell_view(k: EK, white: bool, level: u32, view: u32) -> Rect { let i = sprite_index(k); let row = (view.min(VIEWS - 1) * GEAR_TIERS + gear_tier(level)) * 2 + white as u32; Rect::new((i * SPR_CELL) as f32, (row * SPR_CELL) as f32, SPR_CELL as f32, SPR_CELL as f32) } pub fn tile_tex(kind: Kind, owner: Owner, var: u8, reinforced: bool) -> u32 { let claimed = owner == Owner::Player; let hero = owner == Owner::Hero; let v = var as u32 & 3; match kind { Kind::Rock => T_ROCK + v, Kind::Earth if reinforced && hero => T_FORTWALL + v, Kind::Earth if reinforced => T_REINF + v, Kind::Earth => T_EARTH + v, Kind::Gold => T_GOLD + v, Kind::Silver => T_SILVER + (v & 1), Kind::Ruby => T_RUBY + (v & 1), Kind::Diamond => T_DIAMOND + (v & 1), Kind::Floor => { if claimed { T_CLAIM + v } else if owner == Owner::Rival1 { T_RIVAL1 + v } else if owner == Owner::Rival2 { T_RIVAL2 + v } else if owner == Owner::Rival3 { T_RIVAL3 + v } else if hero { T_HEROFLOOR + v } else { T_FLOOR + v } } Kind::Treasury => T_TREASURY + v, Kind::Lair => T_LAIR + v, Kind::Farm => T_FARM + v, Kind::Training => T_TRAINING + v, Kind::Library => T_LIBRARY + v, Kind::Core => T_COREFLOOR + v, Kind::Portal => T_PORTALFLOOR + v, Kind::Gate => T_GATEFLOOR + v, Kind::Torture => T_TORTURE + v, Kind::Temple => T_TEMPLE + v, Kind::Tower => T_CLAIM + v, Kind::Water => T_WATER + v, Kind::Lava => T_LAVA + v, Kind::Bridge => T_BRIDGE + (v & 1), Kind::LavaBridge => T_STONEBRIDGE + (v & 1), Kind::Shard => T_HEROFLOOR + v, Kind::Aerie => T_AERIE + v, Kind::Steel => T_STEEL + v, Kind::ManaSpring => T_PORTALFLOOR + v, Kind::RuneChest => T_FLOOR + v, Kind::Forge => T_FORGE + v, Kind::Prison => T_PRISON + v, Kind::Graveyard => T_GRAVEYARD + v, Kind::GuardPost => T_GUARDPOST + v, Kind::Arena => T_ARENA + v, Kind::Altar => T_ALTAR + v, Kind::Door => { if claimed { T_CLAIM + v } else { T_FLOOR + v } } } } pub fn draw_cell(&self, id: u32, x: f32, y: f32, w: f32, h: f32, color: Color) { draw_texture_ex( &self.atlas, x, y, color, DrawTextureParams { dest_size: Some(vec2(w, h)), source: Some(Self::cell(id)), ..Default::default() }, ); } // draw a single character (x, y = top-left corner) pub fn glyph(&self, ch: char, x: f32, y: f32, scale: f32, color: Color) { let (sx, sy) = font_src(&self.font_map, ch); let src = Rect::new(sx, sy, GW, GH); draw_texture_ex( &self.font, x.floor(), y.floor(), color, DrawTextureParams { dest_size: Some(vec2(GW * scale, GH * scale)), source: Some(src), ..Default::default() }, ); } // draw text (x, y = top-left corner); scale should be an integer pub fn text(&self, s: &str, x: f32, y: f32, scale: f32, color: Color) { let mut cx = x; for ch in s.chars() { if ch != ' ' { self.glyph(ch, cx, y, scale, color); } cx += GW * scale; } } pub fn measure(&self, s: &str, scale: f32) -> f32 { s.chars().count() as f32 * GW * scale } // Military rank badge for a level (k = size of one badge pixel in device pixels): // Lv 2-4: 1-3 bronze chevrons, Lv 5-7: 1-3 silver bars, Lv 8-9: 1-2 gold stars, // Lv 10: a gold star in a green wreath. Lv 1 has none. Returns the width drawn. pub fn badge(&self, x: f32, y: f32, level: u32, k: f32) -> f32 { let px = |gx: i32, gy: i32, c: Color| draw_rectangle(x + gx as f32 * k, y + gy as f32 * k, k, k, c); let plate = |w: i32, h: i32| draw_rectangle(x - k, y - k, (w + 2) as f32 * k, (h + 2) as f32 * k, Color::new(0.05, 0.03, 0.04, 0.85)); let bronze = Color::from_rgba(214, 140, 70, 255); let silver = Color::from_rgba(215, 222, 235, 255); let gold = Color::from_rgba(255, 210, 70, 255); let star = |ox: i32, oy: i32, c: Color| { for (gx, gy) in [(2, 0), (1, 1), (2, 1), (3, 1), (0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (1, 3), (2, 3), (3, 3), (0, 4), (1, 4), (3, 4), (4, 4)] { px(ox + gx, oy + gy, c); } }; match level { 0 | 1 => 0.0, 2..=4 => { let n = (level - 1) as i32; plate(5, n * 2 + 1); for c in 0..n { // a chevron pointing up for (gx, gy) in [(2, 0), (1, 1), (3, 1), (0, 2), (4, 2)] { px(gx, c * 2 + gy - if gy == 2 { 1 } else { 0 }, bronze); } } 7.0 * k } 5..=7 => { let n = (level - 4) as i32; plate(5, n * 2 - 1); for b in 0..n { for gx in 0..5 { px(gx, b * 2, silver); } } 7.0 * k } 8 | 9 => { let n = (level - 7) as i32; plate(n * 6 - 1, 5); for s in 0..n { star(s * 6, 0, gold); } (n * 6 + 1) as f32 * k } _ => { plate(9, 7); let green = Color::from_rgba(90, 170, 80, 255); for (gx, gy) in [(0, 2), (0, 3), (0, 4), (1, 5), (2, 6), (8, 2), (8, 3), (8, 4), (7, 5), (6, 6), (3, 6), (5, 6), (1, 1), (7, 1)] { px(gx, gy, green); } star(2, 1, gold); 11.0 * k } } } // text with a dark shadow pub fn text_sh(&self, s: &str, x: f32, y: f32, scale: f32, color: Color) { self.text(s, x + scale, y + scale, scale, Color::new(0.0, 0.0, 0.0, color.a * 0.85)); self.text(s, x, y, scale, color); } // window with a box-drawing frame; size in characters (cols x rows incl. frame) pub fn window(&self, x: f32, y: f32, cols: i32, rows: i32, scale: f32, double: bool, frame: Color, bg: Color) { let cw = GW * scale; let ch = GH * scale; draw_rectangle(x, y, cols as f32 * cw, rows as f32 * ch, bg); let b = if double { BOX_DOUBLE } else { BOX_SINGLE }; for cx in 0..cols { for cy in 0..rows { let top = cy == 0; let bottom = cy == rows - 1; let left = cx == 0; let right = cx == cols - 1; let code = match (top, bottom, left, right) { (true, _, true, _) => b[0], (true, _, _, true) => b[1], (_, true, true, _) => b[2], (_, true, _, true) => b[3], (true, _, _, _) | (_, true, _, _) => b[4], (_, _, true, _) | (_, _, _, true) => b[5], _ => continue, }; self.glyph(code, x + cx as f32 * cw, y + cy as f32 * ch, scale, frame); } } } // window with a title in the top frame line pub fn window_titled(&self, x: f32, y: f32, cols: i32, rows: i32, scale: f32, double: bool, frame: Color, bg: Color, title: &str, tcol: Color) { self.window(x, y, cols, rows, scale, double, frame, bg); if !title.is_empty() { let cw = GW * scale; let t = format!(" {} ", title); let n = t.chars().count() as i32; // the frame line ends cleanly on both sides of the title (rope ends, or the double // strand's caps), if there is room for them let caps = n + 5 <= cols; let tx = x + if caps { 1.0 } else { 2.0 } * cw; let w = cw * (n + if caps { 2 } else { 0 }) as f32; draw_rectangle(tx, y, w, GH * scale, bg); if caps { let (l, r) = if double { ('╡', '╞') } else { ('╴', '╶') }; self.glyph(l, tx, y, scale, frame); self.text(&t, tx + cw, y, scale, tcol); self.glyph(r, tx + cw * (n + 1) as f32, y, scale, frame); } else { self.text(&t, tx, y, scale, tcol); } } } } #[cfg(test)] mod doc_images { use super::*; // writes a picture scaled up with nearest pixels fn save(img: &Image, src: Rect, scale: u32, path: &str) { let (w, h) = (src.w as u32 * scale, src.h as u32 * scale); let mut out = Image::gen_image_color(w as u16, h as u16, Color::new(0.0, 0.0, 0.0, 0.0)); for y in 0..h { for x in 0..w { let p = img.get_pixel(src.x as u32 + x / scale, src.y as u32 + y / scale); // export_png writes the rows bottom up: store them flipped out.set_pixel(x, h - 1 - y, p); } } out.export_png(path); } // `cargo test --release export_doc_images -- --ignored` refreshes the pictures in docs/img #[test] #[ignore] fn export_doc_images() { std::fs::create_dir_all("docs/img/units").unwrap(); std::fs::create_dir_all("docs/img/icons").unwrap(); let sprites = gen_sprites(); for &k in SPRITE_KINDS.iter() { let id = crate::game::st(k).name.trim_start_matches("unit."); for (lv, tag) in [(1u32, "1"), (5, "5"), (10, "10")] { save(&sprites, Gfx::sprite_cell(k, false, lv), 8, &format!("docs/img/units/{}_{}.png", id, tag)); } } let atlas = gen_atlas(); for (k, t) in crate::traps::TRAP_KINDS.iter().enumerate() { save(&atlas, Gfx::cell(I_TRAPS + k as u32), 4, &format!("docs/img/icons/{}.png", t.id())); } for (k, s) in crate::spells::SPELLS.iter().enumerate() { save(&atlas, Gfx::cell(I_SPELLS + k as u32), 4, &format!("docs/img/icons/{}.png", s.id())); } for (id, cell) in [("lightning", I_BOLT), ("heal", I_HEAL), ("summon", I_SUMMON), ("arrow", I_ARROW), ("cannon", I_CANNON), ("magic", I_MAGIC), ("rune", I_RUNE)] { save(&atlas, Gfx::cell(cell), 4, &format!("docs/img/icons/{}.png", id)); } for (k, id) in crate::game::ROOM_NAMES.iter().enumerate() { let tex = crate::ui::tool_info(crate::ui::Tool::Room(k)).icon; save(&atlas, Gfx::cell(tex), 4, &format!("docs/img/icons/{}.png", id.trim_start_matches("room."))); } } } #[cfg(test)] mod font_tests { use super::*; // the glyph of a character as rows of '#' and '.' fn bitmap(img: &Image, ch: char) -> Vec { let g = font_slot(ch).expect("slot"); let (gx, gy) = ((g % FONT_COLS) * 6, (g / FONT_COLS) * 8); let w = img.width as usize; let d = img.get_image_data(); (0..8).map(|y| (0..6).map(|x| if d[(gy + y) * w + gx + x][3] > 0 { '#' } else { '.' }).collect()).collect() } #[test] fn dinowyde_slots_match_the_sheet() { let (img, _) = load_font(); assert_eq!(bitmap(&img, 'A'), ["......", ".###..", "#...#.", "#...#.", "#####.", "#...#.", "#...#.", "......"]); assert_eq!(bitmap(&img, 'Ж'), ["......", "#.#.#.", "#.#.#.", ".###..", "#.#.#.", "#.#.#.", "#.#.#.", "......"]); assert_eq!(bitmap(&img, 'Ο')[2..4], [".###..", "#...#."], "Greek capital omicron"); assert!(bitmap(&img, '▀')[0..4].iter().all(|r| r == "######"), "upper half block"); assert_eq!(bitmap(&img, '–')[4], ".###..", "en dash"); assert_eq!(bitmap(&img, '—')[4], "#####.", "em dash"); } #[test] fn missing_glyphs_come_from_dinobyte() { let (_, map) = load_font(); let from_dbyte = |ch: char| font_src(&map, ch).0 >= DBYTE_X as f32; assert!(!from_dbyte('A') && !from_dbyte('ö') && !from_dbyte('Ж') && !from_dbyte('─'), "Dinowyde first"); for ch in ['↑', '↓', '→', '←', '☺', '♥', '♪', '☼'] { assert!(from_dbyte(ch), "{} from DINOBYTE", ch); } } #[test] fn every_text_of_the_game_has_its_glyphs() { let (_, map) = load_font(); let question = font_src(&map, '?'); let mut missing = std::collections::BTreeSet::new(); // every language file in lang/ let files: Vec = std::fs::read_dir("lang") .expect("lang directory") .filter_map(|e| e.ok()) .filter(|e| e.path().extension().map_or(false, |x| x == "lang")) .map(|e| std::fs::read_to_string(e.path()).expect("language file")) .collect(); assert!(files.len() >= 2); for file in &files { for ch in file.chars().filter(|c| !c.is_control() && *c != ' ') { if ch != '?' && font_src(&map, ch) == question { missing.insert(ch); } } } // interface symbols used in the code for ch in ['█', '░', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '▼', '▲', '☻', '≡', '☼', '●', '○', '✓', '—', '·', '↑', '↓', '→', '←', '♥', '♪', '┌', '┐', '└', '┘', '─', '│', '╴', '╶', '╔', '╗', '╚', '╝', '═', '║', '╡', '╞'] { if font_src(&map, ch) == question { missing.insert(ch); } } assert!(missing.is_empty(), "no glyph for {:?}", missing); // Cyrillic and Greek are there for later languages for ch in "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёΑΒΓΔαβγδ".chars() { assert_ne!(font_src(&map, ch), question, "{}", ch); } } }