// Procedurally generated pixel art: tile atlas, unit sprites and the bitmap font. use crate::game::EK; use crate::map::Kind; use macroquad::prelude::*; pub const CELL: f32 = 16.0; const ATLAS_COLS: u32 = 16; const ATLAS_ROWS: u32 = 6; // 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 struct Gfx { pub atlas: Texture2D, pub sprites: Texture2D, pub font: Texture2D, // DINOBYTE, CP437, 16x16 glyphs of 6x8 px } // ------------------------------------------------------------------ bitmap font (CP437) pub const GW: f32 = 6.0; // glyph width pub const GH: f32 = 8.0; // glyph height const CP437_LOW: &str = " ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼"; const CP437_HIGH: &str = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}"; pub fn cp437(ch: char) -> u8 { // DINOBYTE has ö (0x94) and ò (0x95) swapped match ch { 'ö' => return 0x95, 'ò' => return 0x94, _ => {} } let u = ch as u32; if (32..127).contains(&u) { return u as u8; } if ch == '⌂' { return 127; } if let Some(i) = CP437_LOW.chars().position(|c| c == ch) { if i > 0 { return i as u8; } } if let Some(i) = CP437_HIGH.chars().position(|c| c == ch) { return 128 + i as u8; } match ch { 'ẞ' => 0xE1, '–' | '—' => b'-', '„' | '“' | '”' => b'"', '‚' | '‘' | '’' => b'\'', '…' => b'.', '×' => b'x', _ => b'?', } } // frame glyphs: [top-left, top-right, bottom-left, bottom-right, horizontal, vertical] pub const BOX_SINGLE: [u8; 6] = [0xDA, 0xBF, 0xC0, 0xD9, 0xC4, 0xB3]; pub const BOX_DOUBLE: [u8; 6] = [0xC9, 0xBB, 0xC8, 0xBC, 0xCD, 0xBA]; 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 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 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)) }); } // 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_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 } }); 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::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), _ => return None, }) } pub const SPRITE_KINDS: [EK; 10] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Paladin]; pub fn sprite_index(k: EK) -> u32 { SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32 } // row 0: normal sprites, row 1: 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 * SPR_CELL) as u16, Color::new(0.0, 0.0, 0.0, 0.0)); for (i, &k) in SPRITE_KINDS.iter().enumerate() { let rows = sprite_rows(k); for (y, row) in rows.iter().enumerate() { for (x, ch) in row.chars().enumerate().take(SPR) { if let Some(col) = pal(ch) { let px = i as u32 * SPR_CELL + 1 + x as u32; let py = 1 + y as u32; img.set_pixel(px, py, col); img.set_pixel(px, py + SPR_CELL, WHITE); } } } } 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); // load the font; transparent background, white glyphs (for tinting) let mut img = Image::from_file_with_format(include_bytes!("../assets/dbyte_1x.png"), Some(ImageFormat::Png)).expect("Schrift"); for px in img.get_image_data_mut().iter_mut() { let on = px[3] > 128 && px[0] > 128; *px = if on { [255, 255, 255, 255] } else { [0, 0, 0, 0] }; } let font = Texture2D::from_image(&img); font.set_filter(FilterMode::Nearest); Gfx { atlas, sprites, font } } pub fn cell(id: u32) -> Rect { Rect::new((id % ATLAS_COLS) as f32 * CELL, (id / ATLAS_COLS) as f32 * CELL, CELL, CELL) } 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) } pub fn tile_tex(kind: Kind, claimed: bool, hero: bool, var: u8, reinforced: bool) -> u32 { let v = var as u32 & 3; match kind { Kind::Rock => T_ROCK + v, Kind::Earth if reinforced => T_REINF + v, Kind::Earth => T_EARTH + v, Kind::Gold => T_GOLD + v, Kind::Floor => { if claimed { T_CLAIM + 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::Tower => T_CLAIM + 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 CP437 glyph (x, y = top-left corner) pub fn glyph(&self, code: u8, x: f32, y: f32, scale: f32, color: Color) { let src = Rect::new((code % 16) as f32 * GW, (code / 16) as f32 * GH, 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(cp437(ch), cx, y, scale, color); } cx += GW * scale; } } pub fn measure(&self, s: &str, scale: f32) -> f32 { s.chars().count() as f32 * GW * scale } // 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 CP437 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 tx = x + 2.0 * cw; draw_rectangle(tx, y, self.measure(&t, scale), GH * scale, bg); self.text(&t, tx, y, scale, tcol); } } }