// 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; pub const ATLAS_COLS: u32 = 16; pub const ATLAS_ROWS: u32 = 18; // rows 14-17: cells only the high-res atlases fill (see HD_SETS) // 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 const I_DRAWBRIDGE: u32 = 193; // drawbridge tool: a raised flap on chains pub const I_FREEZE: u32 = 194; // frost spell: a snowflake pub const T_OBSIDIAN: u32 = 195; // 4 variants: cooled lava, dark glass with glowing seams pub const T_STEM: u32 = 199; // giant mushroom stem pub const T_CAP: u32 = 200; // giant mushroom cap, glowing spots pub const T_BARK: u32 = 201; // tree trunk pub const T_LEAVES: u32 = 202; // 2 variants: tree crown pub const T_GRASS: u32 = 204; // 4 variants: grass above ground pub const I_WALL: u32 = 208; // wall tool: bricks rising from the floor pub const I_RAM: u32 = 209; // battering ram tool pub const I_CATAPULT: u32 = 210; // catapult tool pub const T_SNOW: u32 = 211; // 4 variants: snow on the land above ground (winter) pub const T_PUMPKIN: u32 = 215; // a carved pumpkin (Halloween) pub const T_MOSS: u32 = 216; // 4 variants: mushroom grove ground, moss with glowing spores pub const T_GIFT: u32 = 220; // a wrapped present (Christmas) pub const T_FIR: u32 = 221; // fir needles with baubles (Christmas) pub const T_EGG: u32 = 222; // 2 variants: a painted egg (Easter) pub struct Gfx { pub atlas: Texture2D, pub sprites: Texture2D, pub hd_sprites: Texture2D, // detailed sprites, 64x64 level (see HD_SPRITES, hd_picture) pub hd_sprites32: Texture2D, // the same, 32x32 level pub gear: Texture2D, // their gear parts (see GEAR_HELMET etc.) pub atlas_hd: Texture2D, // the tile atlas of the high-res style, 64x64 tiles (see HD_TILES) pub atlas_hd2: Texture2D, // the same with 128x128 tiles (close up) pub materials: [Texture2D; 9], // furniture materials, high-res (see crate::props::Mat) pub materials_px: [Texture2D; 9], // the same for the pixel-art style (16x16) pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*) pub badges: Texture2D, // achievement badges, 32x32 each (see badges.rs) pub font: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE pub font_map: FontMap, // which glyphs the two fonts hold } // ------------------------------------------------------------------ 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) } thread_local! { // the interface draws atlas cells from the high-res atlas (the high-res graphics style) static HIGH_RES_UI: std::cell::Cell = const { std::cell::Cell::new(false) }; } pub fn set_high_res_ui(on: bool) { HIGH_RES_UI.with(|h| h.set(on)); } pub fn high_res_ui() -> bool { HIGH_RES_UI.with(|h| h.get()) } // ---- furniture materials (crate::props::Mat order): grey detail the furniture's colours tint // (the page keeps its colours), 64x64 and 16x16; see art/generated/textures/materials/ macro_rules! material { ($name:literal) => { (include_bytes!(concat!("../assets/textures/mat/", $name, "-64.png")) as &[u8], include_bytes!(concat!("../assets/textures/mat/", $name, "-16.png")) as &[u8]) }; } const MATERIALS: [(&[u8], &[u8]); 9] = [ material!("wood"), material!("stone"), material!("iron"), material!("cloth"), material!("straw"), material!("fur"), material!("book"), material!("paper"), material!("crystal"), ]; // ---- high-res tiles // The high-res style's atlases: the same cells as the 16x16 atlas, HD_TILE (64) or HD_TILE2 (128) // texels each; the world shows the 128 one close up, where a tile gets that many pixels (two // levels like the sprites: neither loses pixels nor flickers). Cells without an own texture are // the 16x16 ones scaled up. HD_TILES: generated textures (see art/generated/textures/): for kinds // with four variants a seamless 2x2 block of tiles (variant k = quarter k % 2, k / 2; the world // lays the quarters out by the tiles' positions, see hd_tile, so a floor repeats only every two // tiles and has no seams), for kinds with two variants one seamless tile for both. pub const HD_TILE: u32 = 64; pub const HD_TILE2: u32 = 128; // the liquids: (first cell, variants, 64x64 level, 128x128 level); a seamless 2x2 block of // tiles each (variant k = quarter k % 2, k / 2), which the shader lays out by position while // the texture flows const HD_TILES: [(u32, u32, &[u8], &[u8]); 2] = [ (T_WATER, 4, include_bytes!("../assets/textures/water.png"), include_bytes!("../assets/textures/water-256.png")), (T_LAVA, 4, include_bytes!("../assets/textures/lava.png"), include_bytes!("../assets/textures/lava-256.png")), ]; // Sets of five tiles that fit together in any order, so a floor or a wall shows no pattern: // the ground kinds (all five tiles share one seamless border, their middles differ; see // tools/make_tile.py --variants), the masters' claimed floors as well: fitted flagstones of // different sizes, some cracked, chipped or broken, in the master's colour with faint carved // symbols of their own (the player crimson with demonic runes, Mossgrim mossy green with // mushrooms and mould, Lady Nightwell midnight blue with lilies and moons, Saffrax ochre with // alchemical sigils). They go // into cells of their own from T_HDSET on, five per set; a tile picks one by its position, // weighted. (the kind's cells: first and count, the first own cell, 64x64 tiles side by side, // 128x128 ones, weights) pub const T_HDSET: u32 = 224; // the decals (wear and tear laid over plain floors) and the upright objects (treasury heaps, // mushrooms, the core shard) in the high-res style: transparent pictures for their cells // (cell, 64x64, 128x128) macro_rules! hd_obj { ($id:expr, $name:literal) => { ($id, include_bytes!(concat!("../assets/textures/hd/obj-", $name, "-64.png")), include_bytes!(concat!("../assets/textures/hd/obj-", $name, "-128.png"))) }; } const HD_DECALS: [(u32, &[u8], &[u8]); 8] = [ (D_RUBBLE, include_bytes!("../assets/textures/decal-rubble.png"), include_bytes!("../assets/textures/decal-rubble-128.png")), (D_CRACK, include_bytes!("../assets/textures/decal-crack.png"), include_bytes!("../assets/textures/decal-crack-128.png")), hd_obj!(D_COINS, "coins"), hd_obj!(D_SILVER, "silver"), hd_obj!(D_RUBY, "ruby"), hd_obj!(D_DIAMOND, "diamond"), hd_obj!(D_MUSHROOM, "mushroom"), hd_obj!(D_SHARD, "shard"), ]; // the interface icons of the high-res style (art/generated/icons/): strips of icons, 64x64 and // 128x128, for these cells in this order macro_rules! hd_icons { ($name:literal, $cells:expr) => { (include_bytes!(concat!("../assets/textures/icons/", $name, "-64.png")) as &[u8], include_bytes!(concat!("../assets/textures/icons/", $name, "-128.png")) as &[u8], $cells) }; } const HD_ICONS: [(&[u8], &[u8], &[u32]); 4] = [ hd_icons!("tools", &[I_PICK, I_HAND, I_SELL, I_FLAG, I_BOLT, I_HEAL, I_SUMMON, I_BRIDGE, I_WALL, I_STEEL]), hd_icons!("towers", &[I_ARROW, I_CANNON, I_MAGIC, I_RUNE, I_DRAWBRIDGE, I_RAM, I_CATAPULT, I_ORDERS, I_ORDERS + 1, I_FREEZE]), hd_icons!("traps", &[I_TRAPS, I_TRAPS + 1, I_TRAPS + 2, I_TRAPS + 3, I_TRAPS + 4, I_TRAPS + 5, I_TRAPS + 6, I_TRAPS + 7, I_TRAPS + 8, I_TRAPS + 9]), hd_icons!("spells", &[I_SPELLS, I_SPELLS + 1, I_SPELLS + 2, I_SPELLS + 3, I_SPELLS + 4, I_SPELLS + 5, I_SPELLS + 6, I_SPELLS + 7]), ]; // Everything else in the world (see art/generated/textures/all/): a kind's own cells get as many // variants as it has cells, tiles that fit together in any order (one seamless border, different // middles), and a tile picks one by its position. (first cell, cells, 64x64 tiles side by side, // 128x128 ones) macro_rules! hd_var { ($id:expr, $n:expr, $name:literal) => { ($id, $n, include_bytes!(concat!("../assets/textures/hd/", $name, "-v64.png")), include_bytes!(concat!("../assets/textures/hd/", $name, "-v128.png"))) }; } const HD_VARS: [(u32, u32, &[u8], &[u8]); 31] = [ hd_var!(T_FORTWALL, 4, "fortwall"), hd_var!(T_STEEL, 4, "steel"), hd_var!(T_OBSIDIAN, 4, "obsidian"), hd_var!(T_FACE_EARTH, 1, "face"), hd_var!(T_TREASURY, 4, "treasury"), hd_var!(T_LAIR, 4, "lair"), hd_var!(T_FARM, 4, "farm"), hd_var!(T_TRAINING, 4, "training"), hd_var!(T_LIBRARY, 4, "library"), hd_var!(T_TORTURE, 4, "torture"), hd_var!(T_TEMPLE, 4, "temple"), hd_var!(T_PRISON, 4, "prison"), hd_var!(T_GRAVEYARD, 4, "graveyard"), hd_var!(T_GUARDPOST, 4, "guardpost"), hd_var!(T_ARENA, 4, "arena"), hd_var!(T_ALTAR, 4, "altar"), hd_var!(T_FORGE, 4, "forge"), hd_var!(T_AERIE, 4, "aerie"), hd_var!(T_COREFLOOR, 4, "core"), hd_var!(T_PORTALFLOOR, 4, "portal"), hd_var!(T_GATEFLOOR, 4, "gate"), hd_var!(T_HEROFLOOR, 4, "hero"), hd_var!(T_BRIDGE, 2, "bridge"), hd_var!(T_STONEBRIDGE, 2, "stonebridge"), hd_var!(T_GRASS, 4, "grass"), hd_var!(T_SNOW, 4, "snow"), hd_var!(T_MOSS, 4, "moss"), hd_var!(T_LEAVES, 2, "leaves"), hd_var!(T_BARK, 1, "bark"), hd_var!(T_STEM, 1, "stem"), hd_var!(T_CAP, 1, "cap"), ]; const HD_SETS: [(u32, u32, u32, &[u8], &[u8], [u32; 5]); 12] = [ (T_CLAIM, 4, T_HDSET, include_bytes!("../assets/textures/floor-player.png"), include_bytes!("../assets/textures/floor-player-128.png"), [1; 5]), (T_RIVAL1, 4, T_HDSET + 5, include_bytes!("../assets/textures/floor-mossgrim.png"), include_bytes!("../assets/textures/floor-mossgrim-128.png"), [1; 5]), (T_RIVAL2, 4, T_HDSET + 10, include_bytes!("../assets/textures/floor-nightwell.png"), include_bytes!("../assets/textures/floor-nightwell-128.png"), [1; 5]), (T_RIVAL3, 4, T_HDSET + 15, include_bytes!("../assets/textures/floor-saffrax.png"), include_bytes!("../assets/textures/floor-saffrax-128.png"), [1; 5]), (T_FLOOR, 4, T_HDSET + 20, include_bytes!("../assets/textures/floor-v64.png"), include_bytes!("../assets/textures/floor-v128.png"), [1; 5]), (T_EARTH, 4, T_HDSET + 25, include_bytes!("../assets/textures/earth-v64.png"), include_bytes!("../assets/textures/earth-v128.png"), [1; 5]), (T_ROCK, 4, T_HDSET + 30, include_bytes!("../assets/textures/rock-v64.png"), include_bytes!("../assets/textures/rock-v128.png"), [1; 5]), (T_GOLD, 4, T_HDSET + 35, include_bytes!("../assets/textures/gold-v64.png"), include_bytes!("../assets/textures/gold-v128.png"), [1; 5]), (T_SILVER, 2, T_HDSET + 40, include_bytes!("../assets/textures/silver-v64.png"), include_bytes!("../assets/textures/silver-v128.png"), [1; 5]), (T_RUBY, 2, T_HDSET + 45, include_bytes!("../assets/textures/ruby-v64.png"), include_bytes!("../assets/textures/ruby-v128.png"), [1; 5]), (T_DIAMOND, 2, T_HDSET + 50, include_bytes!("../assets/textures/diamond-v64.png"), include_bytes!("../assets/textures/diamond-v128.png"), [1; 5]), (T_REINF, 4, T_HDSET + 55, include_bytes!("../assets/textures/reinforced-v64.png"), include_bytes!("../assets/textures/reinforced-v128.png"), [1; 5]), ]; // the cell a tile at (x, z) shows in the high-res style: the kinds with a set pick one of its // five tiles (HD_SETS), the liquids are laid out by the position (HD_TILES), everything else stays pub fn hd_tile(id: u32, x: i32, z: i32) -> u32 { if let Some(&(_, _, own, _, _, w)) = HD_SETS.iter().find(|f| (f.0..f.0 + f.1).contains(&id)) { let h = (x as u32).wrapping_mul(73_856_093) ^ (z as u32).wrapping_mul(19_349_663); let h = (h ^ (h >> 13)).wrapping_mul(0x5bd1_e995); let mut r = (h >> 8) % w.iter().sum::(); for (k, &wk) in w.iter().enumerate() { if r < wk { return own + k as u32; } r -= wk; } } if let Some(&(base, n, _, _)) = HD_VARS.iter().find(|&&(base, n, _, _)| (base..base + n).contains(&id)) { let h = (x as u32).wrapping_mul(2_654_435_761) ^ (z as u32).wrapping_mul(40_503); let h = (h ^ (h >> 15)).wrapping_mul(0x2c1b_3c6d); return base + (h >> 9) % n; } match HD_TILES.iter().find(|&&(base, n, _, _)| (base..base + n).contains(&id)) { Some(&(base, 4, _, _)) => base + (x & 1) as u32 + (z & 1) as u32 * 2, _ => id, } } // mean and standard deviation per channel of the opaque texels of some cells fn cell_stats(img: &Image, cells: &[u32], tile: u32) -> ([f32; 3], [f32; 3]) { let (mut sum, mut sq, mut n) = ([0f32; 3], [0f32; 3], 0f32); for &id in cells { let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile); for y in 0..tile { for x in 0..tile { let p = img.get_pixel(ox + x, oy + y); if p.a < 0.5 { continue; } for (c, v) in [p.r, p.g, p.b].into_iter().enumerate() { sum[c] += v; sq[c] += v * v; } n += 1.0; } } } let n = n.max(1.0); let mean = sum.map(|s| s / n); let mut sd = [0f32; 3]; for c in 0..3 { sd[c] = (sq[c] / n - mean[c] * mean[c]).max(0.0).sqrt(); } (mean, sd) } // moves the colours of the high-res cells `dst` towards those of the 16x16 cells `src` fn match_colors(img: &mut Image, small: &Image, src: &[u32], dst: &[u32], tile: u32) { const STRENGTH: f32 = 0.8; let (pm, ps) = cell_stats(small, src, 16); let (hm, hs) = cell_stats(img, dst, tile); for &id in dst { let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile); for y in 0..tile { for x in 0..tile { let p = img.get_pixel(ox + x, oy + y); if p.a < 0.5 { continue; } let mut q = [p.r, p.g, p.b]; for c in 0..3 { let k = (ps[c] / hs[c].max(0.01)).clamp(0.6, 1.6); let m = pm[c] + (q[c] - hm[c]) * k; q[c] = (q[c] + (m - q[c]) * STRENGTH).clamp(0.0, 1.0); } img.set_pixel(ox + x, oy + y, Color::new(q[0], q[1], q[2], p.a)); } } } } // a high-res atlas with `tile` x `tile` texel cells (HD_TILE or HD_TILE2) fn gen_hd_atlas(tile: u32) -> Image { let small = gen_atlas(); let k = tile / 16; let (w, h) = (ATLAS_COLS * tile, ATLAS_ROWS * tile); let mut img = 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 { img.set_pixel(x, y, small.get_pixel(x / k, y / k)); } } for &(base, n, png, png2) in HD_TILES.iter() { let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd tile"); for v in 0..n { let (sx, sy) = if n == 4 { ((v % 2) * tile, (v / 2) * tile) } else { (0, 0) }; let id = base + v; let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile); for y in 0..tile { for x in 0..tile { img.set_pixel(ox + x, oy + y, src.get_pixel(sx + x, sy + y)); } } } } for &(_, _, own, png, png2, _) in HD_SETS.iter() { let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd set"); for v in 0..5 { let id = own + v; let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile); for y in 0..tile { for x in 0..tile { img.set_pixel(ox + x, oy + y, src.get_pixel(v * tile + x, y)); } } } } for &(base, n, png, png2) in HD_VARS.iter() { let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd variants"); for v in 0..n { let id = base + v; let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile); for y in 0..tile { for x in 0..tile { img.set_pixel(ox + x, oy + y, src.get_pixel(v * tile + x, y)); } } } } for &(id, png, png2) in HD_DECALS.iter() { let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd decal"); let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile); for y in 0..tile { for x in 0..tile { img.set_pixel(ox + x, oy + y, src.get_pixel(x, y)); } } } for &(png, png2, cells) in HD_ICONS.iter() { let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd icons"); for (v, &id) in cells.iter().enumerate() { let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile); for y in 0..tile { for x in 0..tile { img.set_pixel(ox + x, oy + y, src.get_pixel(v as u32 * tile + x, y)); } } } } // the generated pictures take on the colours of the 16x16 cells they replace (mean and // contrast per channel, most of the way), so switching between the styles keeps the look let range = |a: u32, n: u32| (a..a + n).collect::>(); let mut groups: Vec<(Vec, Vec)> = Vec::new(); for &(base, n, _, _) in HD_TILES.iter() { groups.push((range(base, n), range(base, n))); } for &(first, n, own, _, _, _) in HD_SETS.iter() { groups.push((range(first, n), range(own, 5))); } for &(base, n, _, _) in HD_VARS.iter() { groups.push((range(base, n), range(base, n))); } for &(id, _, _) in HD_DECALS.iter().skip(2) { groups.push((vec![id], vec![id])); } for (src, dst) in groups.iter() { match_colors(&mut img, &small, src, dst, tile); } img } 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 { // reinforced wall: a castle wall of roughly squared grey blocks in regular courses (three // courses of 6, 5 and 5 pixels, every course cut into blocks of 5-9 pixels at its own places; // wrapped, so the cell is seamless), dark mortar with a crimson tint, the top of every // block lit, its bottom in shadow, now and then a chipped corner cv.cell(T_REINF + v, |x, y| { let (row, ry, rh) = match y { 0..=5 => (0, y, 6), 6..=10 => (1, y - 6, 5), _ => (2, y - 11, 5), }; // the course's cuts: from an offset on, blocks of 5-9 pixels around the cell let off = (hash(row, 1, v + 95) % 6) as i32; let mut cuts = vec![off]; let mut c = off; let mut k = 0; loop { c += 5 + (hash(row, k, v + 96) % 5) as i32; k += 1; if c > off + 16 - 4 { break; } cuts.push(c); } let xs = (x - off).rem_euclid(16) + off; let block = cuts.iter().filter(|&&q| q <= xs).count() as i32; let start = cuts[(block - 1).max(0) as usize]; let end = cuts.get(block as usize).copied().unwrap_or(off + 16); let mortar = ry == rh - 1 || xs == end - 1; let chip = (xs == start || xs == end - 2) && (ry == 0 || ry == rh - 2) && hash(row * 7 + block, ry, v + 97) % 4 == 0; if mortar || chip { return Some(shade((46, 30, 34), (rnd(x, y, v + 98) * 8.0) as i32)); } let tone = (hash(row * 11 + block, 3, v + 99) % 5) as i32 * 5 - 10; let face = if ry == 0 { 12 } else if ry == rh - 2 { -8 } else { 0 }; Some(shade((92, 90, 96), tone + face + (rnd(x, y, v + 100) * 10.0) as i32 - 5)) }); // 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 } }); // drawbridge tool icon: a raised flap with its chains above a river cv.cell(I_DRAWBRIDGE, |x, y| { if (3..=5).contains(&x) && (2..=12).contains(&y) { Some(if y % 3 == 0 { c(70, 46, 26) } else { c(150, 106, 60) }) } else if y == 12 && (5..=13).contains(&x) { Some(c(110, 76, 44)) } else if (x - 5) * 2 == (y - 2) * 1 + 2 && (6..=12).contains(&x) { Some(c(170, 170, 180)) // chain } else if x == 13 && (3..=12).contains(&y) { Some(c(90, 90, 100)) // post } else if y >= 13 && (1..=14).contains(&x) { Some(c(60, 110, 170)) } else { None } }); // wall tool icon: a block of bricks with an arrow up cv.cell(I_WALL, |x, y| { if (3..=12).contains(&x) && (6..=14).contains(&y) { let row = (y - 6) / 3; let joint = (y - 6) % 3 == 2 || (x + row * 3) % 6 == 0; Some(if joint { c(60, 40, 30) } else { c(150, 100, 70) }) } else if y <= 4 && (x - 8).abs() <= 4 - y.max(1) + 1 && y >= 1 { Some(c(200, 140, 255)) } else if y == 5 && (7..=9).contains(&x) { Some(c(200, 140, 255)) } else { None } }); // siege icons: a ram (a trunk with an iron cap under a roof), a catapult (arm and stone) cv.cell(I_RAM, |x, y| { if (3..=12).contains(&x) && y == 4 + (x - 3).min(12 - x).min(3) * 0 && false { None } else if y >= 3 && y <= 5 && (x - 7).abs() <= 6 - (y - 3) * 0 && (x + y) % 2 == 0 { Some(c(92, 58, 32)) } else if (7..=9).contains(&y) && (1..=13).contains(&x) { Some(if x <= 3 { c(170, 175, 190) } else { c(140, 92, 52) }) } else if y >= 11 && y <= 13 && ((x - 4).abs() <= 1 || (x - 11).abs() <= 1) { Some(c(30, 24, 26)) } else if y == 10 && (3..=12).contains(&x) { Some(c(92, 58, 32)) } else { None } }); cv.cell(I_CATAPULT, |x, y| { let arm = x + y == 14 && (3..=11).contains(&x); if (x - 12).pow(2) + (y - 3).pow(2) <= 3 { Some(c(150, 152, 140)) } else if arm { Some(c(140, 92, 52)) } else if y == 11 && (2..=13).contains(&x) { Some(c(92, 58, 32)) } else if y >= 12 && ((x - 4).abs() <= 1 || (x - 11).abs() <= 1) { Some(c(30, 24, 26)) } else { None } }); // frost spell icon: a snowflake cv.cell(I_FREEZE, |x, y| { let (dx, dy) = (x - 8, y - 8); let arm = dx == 0 || dy == 0 || dx == dy || dx == -dy; let r = dx.abs().max(dy.abs()); if arm && r <= 6 { Some(if r <= 1 { c(240, 250, 255) } else { c(150, 210, 255) }) } else if r == 4 && (dx.abs() == 1 || dy.abs() == 1) && (dx == 0 || dy == 0 || dx.abs() == dy.abs() + 1 || dy.abs() == dx.abs() + 1) { Some(c(200, 235, 255)) } else { None } }); // giant mushroom: pale ribbed stem, a violet cap with glowing spots cv.cell(T_STEM, |x, y| { let r = hash(x, y, 410); Some(if x % 4 == 0 { c(150, 140, 130) } else if r % 9 == 0 { c(170, 160, 150) } else { c(200, 192, 178) }) }); cv.cell(T_CAP, |x, y| { let spot = |cx: i32, cy: i32| (x - cx).pow(2) + (y - cy).pow(2) <= 2; if spot(4, 4) || spot(11, 6) || spot(6, 11) || spot(12, 12) { Some(c(150, 255, 230)) } else if (x + y) % 5 == 0 { Some(c(90, 50, 130)) } else { Some(c(120, 66, 170)) } }); // tree: bark and two kinds of crown cv.cell(T_BARK, |x, y| { let r = hash(x, y, 420); Some(if x % 5 == 0 || r % 7 == 0 { c(60, 40, 24) } else { c(96, 66, 40) }) }); for v in 0..2u32 { cv.cell(T_LEAVES + v, |x, y| { let r = hash(x, y, 430 + v); // dark, bluish green: clearly apart from the light meadow Some(if r % 5 == 0 { c(14, 40, 30) } else if r % 7 == 0 { c(52, 96, 62) } else if v == 0 { c(26, 64, 44) } else { c(32, 72, 40) }) }); } // snow above ground in winter for v in 0..4u32 { cv.cell(T_SNOW + v, |x, y| { let r = hash(x, y, 450 + v); Some(if r % 9 == 0 { c(214, 222, 236) } else if r % 23 == 0 { c(150, 170, 120) } else { c(236, 240, 248) }) }); } // a carved pumpkin: orange ribs, a grinning face cv.cell(T_PUMPKIN, |x, y| { let eye = (y == 5 || y == 6) && (x == 4 || x == 5 || x == 10 || x == 11); let mouth = (y == 10 && (3..=12).contains(&x)) || (y == 11 && (5..=10).contains(&x) && x % 2 == 0); if eye || mouth { Some(c(255, 230, 90)) } else if x % 4 == 0 { Some(c(190, 90, 20)) } else { Some(c(240, 128, 30)) } }); // Christmas: a present in red paper with a golden ribbon, fir needles with baubles cv.cell(T_GIFT, |x, y| { if x == 7 || x == 8 || y == 7 || y == 8 { Some(c(250, 210, 80)) } else if (x + y) % 5 == 0 { Some(c(200, 30, 40)) } else { Some(c(176, 24, 34)) } }); cv.cell(T_FIR, |x, y| { let r = hash(x, y, 470); if r % 29 == 0 { Some([c(240, 60, 60), c(250, 210, 80), c(90, 160, 255)][(r / 29 % 3) as usize]) } else if (x + 2 * y) % 5 == 0 { Some(c(20, 70, 40)) } else { Some(shade((34, 104, 56), (rnd(x, y, 471) * 16.0) as i32 - 8)) } }); // Easter: painted eggs, stripes and zigzags in spring colours for v in 0..2u32 { cv.cell(T_EGG + v, |x, y| { let cols = if v == 0 { [c(140, 200, 250), c(250, 230, 120), c(255, 255, 255)] } else { [c(250, 160, 200), c(150, 220, 140), c(255, 250, 230)] }; let zig = (y + (x % 4 - 2).abs()) % 8; Some(match zig { 0 | 1 => cols[1], 4 => cols[2], _ => cols[0], }) }); } // grass above ground for v in 0..4u32 { cv.cell(T_GRASS + v, |x, y| { let r = hash(x, y, 440 + v); // a light, yellowish meadow with flowers Some(if r % 11 == 0 { c(176, 200, 96) } else if r % 6 == 0 { c(112, 150, 62) } else if r % 23 == 0 { c(236, 220, 110) } else if r % 37 == 0 { c(230, 236, 240) } else { c(140, 176, 78) }) }); } // mushroom grove: dark stones overgrown with teal moss, a few glowing spores for v in 0..4u32 { cv.cell(T_MOSS + v, |x, y| { let r = hash(x, y, 520 + v); let moss = (hash(x / 3, y / 3, 530 + v) % 5) < 3; Some(if r % 41 == 0 { c(150, 255, 220) // a spore glows } else if r % 29 == 0 { c(200, 150, 255) } else if moss { if r % 4 == 0 { c(46, 112, 96) } else if r % 3 == 0 { c(30, 84, 74) } else { c(38, 98, 84) } } else { stones(x, y, v, (52, 58, 56), (26, 30, 30), 8) }) }); } // obsidian: cooled lava, dark glass with a few glowing seams for v in 0..4u32 { cv.cell(T_OBSIDIAN + v, |x, y| { let r = hash(x, y, v + 400); if (x * 5 + y * 3 + v as i32 * 7) % 29 == 0 || ((x + 2 * y + v as i32) % 23 == 0 && r % 2 == 0) { Some(c(230, 90, 30)) } else if r % 11 == 0 { Some(c(70, 60, 90)) } else if (x + y) % 7 == 0 { Some(c(34, 26, 40)) } else { Some(c(22, 16, 26)) } }); } // ---- 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; // the atlas holds every sprite pixel as SPR_K x SPR_K texels: a cell is SPR_CELL * SPR_K texels const SPR_K: u32 = 3; pub const ACELL: u32 = SPR_CELL * SPR_K; // Detailed sprites (generated, see art/generated/imp-sprite and art/generated/sprites), used // instead of the 10x10 ones in the high-res graphics style (render3d::billboards). Two levels of // detail from the same pictures, in two textures: 64x64 when the sprite gets enough screen // pixels (close), 32x32 otherwise, so neither loses pixels nor flickers. Sheets: transparent, // the feet on the bottom row; idle: the five views side by side (VIEWS); an animation: // ANIM_FRAMES frames side by side, one row per view. The gear (shoulder pieces, helmet, cape, // crown) is laid over them at draw time (see hd_anchor, GEAR_PARTS); the hit flash is drawn // white by the shader. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum HdAnim { Hack, Walk, Attack, Die, } pub const ANIM_FRAMES: u32 = 4; struct HdSprite { kind: EK, idle: &'static [u8], idle_32: &'static [u8], anims: &'static [(HdAnim, &'static [u8], &'static [u8])], // 64x64 and 32x32 sheets } macro_rules! hd_sprite { ($kind:expr, $name:literal) => { HdSprite { kind: $kind, idle: include_bytes!(concat!("../assets/sprites/", $name, "-64.png")), idle_32: include_bytes!(concat!("../assets/sprites/", $name, "-32.png")), anims: &[], } }; // with walking and attacking (assets/sprites/-walk-64.png etc.) ($kind:expr, $name:literal, animated) => { HdSprite { kind: $kind, idle: include_bytes!(concat!("../assets/sprites/", $name, "-64.png")), idle_32: include_bytes!(concat!("../assets/sprites/", $name, "-32.png")), anims: &[ (HdAnim::Walk, include_bytes!(concat!("../assets/sprites/", $name, "-walk-64.png")), include_bytes!(concat!("../assets/sprites/", $name, "-walk-32.png"))), (HdAnim::Attack, include_bytes!(concat!("../assets/sprites/", $name, "-attack-64.png")), include_bytes!(concat!("../assets/sprites/", $name, "-attack-32.png"))), ], } }; } const HD_SPRITES: [HdSprite; 31] = [ HdSprite { kind: EK::Worker, idle: include_bytes!("../assets/sprites/imp-64.png"), idle_32: include_bytes!("../assets/sprites/imp-32.png"), anims: &[(HdAnim::Hack, include_bytes!("../assets/sprites/imp-hack-64.png"), include_bytes!("../assets/sprites/imp-hack-32.png"))], }, hd_sprite!(EK::Kobold, "kobold", animated), hd_sprite!(EK::Spider, "spider"), hd_sprite!(EK::Mage, "mage"), hd_sprite!(EK::Ogre, "ogre"), hd_sprite!(EK::Knight, "knight", animated), hd_sprite!(EK::Archer, "archer"), hd_sprite!(EK::Priest, "priest"), hd_sprite!(EK::Dwarf, "dwarf"), hd_sprite!(EK::Paladin, "paladin"), hd_sprite!(EK::Jailer, "jailer"), hd_sprite!(EK::Priestess, "priestess"), hd_sprite!(EK::Chainbreaker, "chainbreaker"), hd_sprite!(EK::Bat, "bat"), hd_sprite!(EK::Gargoyle, "gargoyle"), hd_sprite!(EK::Owl, "owl"), hd_sprite!(EK::Wyrm, "wyrm"), hd_sprite!(EK::Cogling, "cogling"), hd_sprite!(EK::Revenant, "revenant"), hd_sprite!(EK::Giant, "giant"), hd_sprite!(EK::Fairy, "fairy"), hd_sprite!(EK::BattleMage, "battlemage"), hd_sprite!(EK::Ram, "ram"), hd_sprite!(EK::Catapult, "catapult"), hd_sprite!(EK::CaveBear, "cavebear"), hd_sprite!(EK::Beetle, "beetle"), hd_sprite!(EK::Trader, "trader"), hd_sprite!(EK::Thief, "thief"), hd_sprite!(EK::Monk, "monk"), hd_sprite!(EK::Ranger, "ranger"), hd_sprite!(EK::Inquisitor, "inquisitor"), ]; // a detailed sprite's cell: the same shape as a 10x10 sprite's 12x12 cell (6 texels per pixel), // so the billboard is the same; the 64x64 picture sits where the 10x10 sprite would; the 32x32 // level has cells of half the size pub const HD_CELL: u32 = SPR_CELL * 6; const HD_COLS64: u32 = 56; const HD_COLS32: u32 = 112; // the pictures one after another: per sprite its frames (idle, then every animation's frames), // per frame without and (kinds with a sack) with the sack, per those the five views fn hd_frames(j: usize) -> u32 { 1 + HD_SPRITES[j].anims.len() as u32 * ANIM_FRAMES } fn hd_sacks(j: usize) -> u32 { if SACK_KINDS.contains(&HD_SPRITES[j].kind) { 2 } else { 1 } } fn hd_base(j: usize) -> u32 { (0..j).map(|i| hd_frames(i) * hd_sacks(i) * VIEWS).sum() } fn hd_pictures() -> u32 { hd_base(HD_SPRITES.len()) } // the size of a detailed sprite texture (fine: the 64x64 one) pub fn hd_tex_size(fine: bool) -> (u32, u32) { let (cols, cell) = if fine { (HD_COLS64, HD_CELL) } else { (HD_COLS32, HD_CELL / 2) }; (cols * cell, hd_pictures().div_ceil(cols) * cell) } // where a picture's gear goes: rows and columns of a 72x72 cell as shares of it (0..1) #[derive(Clone, Copy, Debug, Default)] pub struct GearAnchor { pub top: f32, // top of the head pub bottom: f32, // the feet pub head: (f32, f32), // the head's left and right edge (a little below its top) pub shoulder: f32, // the shoulders' row pub sh: (f32, f32), // the shoulders' left and right edge } static HD_ANCHORS: std::sync::OnceLock> = std::sync::OnceLock::new(); fn gear_anchor(g: &Cell) -> GearAnchor { let n = g.len(); let f = |v: usize| v as f32 / n as f32; let span = |y: usize| -> Option<(usize, usize)> { let xs: Vec = (0..n).filter(|&x| g[y][x].is_some()).collect(); Some((*xs.first()?, *xs.last()?)) }; let rows: Vec = (0..n).filter(|&y| span(y).is_some()).collect(); let (Some(&top), Some(&bot)) = (rows.first(), rows.last()) else { return GearAnchor::default() }; let h = bot - top + 1; let head_y = (top + h / 10).min(bot); let head = span(head_y).unwrap_or((n / 3, 2 * n / 3)); // the shoulders: the widest row of the upper body (tools and wings stick out: the middle // row of the widest few counts) let (a, b) = (top + h * 3 / 10, (top + h / 2).min(bot)); let shoulder = (a..=b).max_by_key(|&y| span(y).map_or(0, |(l, r)| r - l)).unwrap_or(a); let sh = span(shoulder).unwrap_or(head); GearAnchor { top: f(top), bottom: f(bot + 1), head: (f(head.0), f(head.1 + 1)), shoulder: f(shoulder), sh: (f(sh.0), f(sh.1 + 1)) } } // the gear's parts in their texture (grey, tinted by the metal or cloth colour at draw time): // (x, y, w, h) in texels of GEAR_TEX pub const GEAR_TEX: u32 = 64; pub const GEAR_HELMET: (u32, u32, u32, u32) = (0, 0, 24, 12); pub const GEAR_PAULDRON: (u32, u32, u32, u32) = (24, 0, 12, 9); pub const GEAR_CROWN: (u32, u32, u32, u32) = (36, 0, 20, 10); pub const GEAR_CAPE: (u32, u32, u32, u32) = (0, 14, 28, 40); pub const GEAR_HORN: (u32, u32, u32, u32) = (28, 14, 7, 10); pub const GEAR_PLUME: (u32, u32, u32, u32) = (36, 14, 7, 12); // kinds that carry treasure in a sack on the back (instead of the coin sign over the head); // their 10x10 sprite with the sack gets an own atlas column after SPRITE_KINDS const SACK_KINDS: [EK; 1] = [EK::Worker]; // the sack (k outline, B burlap, b its shade and the rope, y/Y gold coins at the opening) const SACK_SMALL: [&str; 5] = [".kk.", "kyYk", "kBbk", "kBBk", ".kk."]; const SACK_32: [&str; 12] = [ "....kkkk....", "...kyYYyk...", "..kyYyyYyk..", "..kkbbbbkk..", "...kBBBBk...", "..kBBBBBBk..", ".kBBBbBBBBk.", ".kBBBBBBbBk.", ".kBbBBBBBBk.", ".kBBBBBBBBk.", "..kBBBBBBk..", "...kkkkkk...", ]; // the same, finer for the 64x64 sprites (d: the shadow side) const SACK_64: [&str; 22] = [ ".......kkkkkk.......", ".....kkyYYYyykk.....", "....kyYYyYYyyYyk....", "...kyYyyYyYYyyYyk...", "...kkyYYyyyYYyykk...", "....kkbbbbbbbbkk....", ".....kbBbbbbBbk.....", "......kBBBBBBk......", ".....kBBBBBBBBk.....", "....kBBBBBBBBBBk....", "...kBBBBbBBBBBBBk...", "..kBBBBBBBBBbBBBBk..", "..kBBbBBBBBBBBBBBk..", ".kBBBBBBBBBBBBbBBBk.", ".kBBBBBBBbBBBBBBBBk.", ".kBBBBBBBBBBBBBBBdk.", ".kBBbBBBBBBBBBBBddk.", ".kBBBBBBBBBBBBBdddk.", "..kBBBBBBBBBBBdddk..", "..kdBBBBBBBBBddddk..", "...kkddddddddddkk...", ".....kkkkkkkkkk.....", ]; 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..", ], // thief: a dark hood and mask, a dagger, a sack of gold on the back EK::Thief => [ "...kkkk...", "..kddddk..", "..kdfkfk.Y", "..kddddkYy", ".kdddddkYy", "skdddddk..", ".kdkdddk..", "..kddddk..", "..kdkkdk..", "..kk..kk..", ], // monk: shaven head, an orange robe tied with a rope, bare fists EK::Monk => [ "...kkkk...", "..kffffk..", "..kfkfkk..", "..kffffk..", ".kooooook.", "kfoooooofk", ".kooyyook.", ".kOooooOk.", ".kOooooOk.", "..kkkkkk..", ], // ranger: a green hood and cloak, a long bow, leather boots EK::Ranger => [ "...kkkk.b.", "..kgGGgkkb", "..kGfkfk.b", "..kGfffk.b", ".kGgggggkb", ".kGgbbggkb", ".kGggggk.b", "..kBBBBk.b", "..kBkkBkb.", "..kk..kk..", ], // inquisitor: a tall red hat, white and red robes, a glowing holy book EK::Inquisitor => [ "...kRRk...", "..kRrrRk..", "..kRrrRk..", "..kffffk..", "..kfkfkk..", ".kwwrrwwk.", "kwkwrrwkek", ".kwwrrwwkk", ".kRwwwwRk.", "..kkkkkk..", ], 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...", "..........", ], // trader: a hooded merchant with a big pack and a lantern EK::Trader => [ "...kkk....", "..kPPPk.BB", ".kPfefPkBb", ".kPffffkBb", "..kPPPPkBB", ".kPPyPPPk.", "ykPPPPPPk.", "o.kPPPPk..", "..kPk.kPk.", "..kk...kk.", ], // cave bear: shaggy brown fur, small yellow eyes EK::CaveBear => [ ".kk....kk.", "kBBk..kBBk", "kBBBBBBBBk", "kBeBBBBeBk", "kBBBkkBBBk", ".kBBffBBk.", "kBBBBBBBBk", "kBbBBBBbBk", ".kBBkkBBk.", ".kkk..kkk.", ], // giant beetle: a shiny dark-green shell, mandibles, six legs EK::Beetle => [ "..k....k..", "...k..k...", "..kGGGGk..", ".kGeGGeGk.", "kGGgGGgGGk", "kGgGGGGgGk", "kGGgGGgGGk", ".kGGGGGGk.", "k.kGGGGk.k", ".k.k..k.k.", ], // battering ram: a roofed frame on wheels, an iron-capped trunk EK::Ram => [ "..........", "...BBBB...", "..BbbbbB..", ".BbbbbbbB.", "SsbbbbbbB.", "SsBBBBBBB.", "..b.b.b.b.", ".kBk..kBk.", ".kck..kck.", "..k....k..", ], // catapult: a throwing arm with a stone in its cup EK::Catapult => [ "......cc..", ".....kCCk.", ".....kbk..", "....kb....", "...kb.....", "..kb......", ".BBBBBBBB.", ".bkbbbbkb.", ".kck..kck.", "..k....k..", ], // 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; 31] = [ 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, EK::Ram, EK::Catapult, EK::CaveBear, EK::Beetle, EK::Trader, EK::Thief, EK::Monk, EK::Ranger, EK::Inquisitor, ]; // width of the sprite texture in pixels pub const SPRITE_TEX_W: f32 = ((SPRITE_KINDS.len() + SACK_KINDS.len()) as u32 * ACELL) as f32; pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * ACELL) 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 | EK::Thief | EK::Monk | EK::Ranger | EK::Inquisitor) } // 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 } type Cell = Vec>>; // a sack on the back of a figure in a cell: from the front it shows beside the body (drawn // behind the figure), from behind on the back (in front of it); `view` as in VIEWS; `front_dx`: // where it shows beside the body in the front and front three-quarter view (in body widths, // the side away from the tool) fn add_sack(g: &mut Cell, view: u32, pat: &[&str], front_dx: (f32, f32)) { let n = g.len(); let rows: Vec = (0..n).filter(|&y| g[y].iter().any(|p| p.is_some())).collect(); let (Some(&top), Some(&bot)) = (rows.first(), rows.last()) else { return }; let h = (bot - top + 1) as f32; // the body's middle: the opaque columns of the torso band (tools and tails stick out) let mut xs: Vec = Vec::new(); for y in top + (h * 0.4) as usize..=top + (h * 0.6) as usize { xs.extend((0..n).filter(|&x| g[y][x].is_some())); } if xs.is_empty() { return; } xs.sort(); let cx = xs[xs.len() / 2] as f32; let span = (xs[xs.len() * 9 / 10] - xs[xs.len() / 10]) as f32; let pw = pat[0].len() as f32; // (offset of the sack's centre from the body's middle in body widths, its top in heights, in front) let (dx, dy, front) = match view { 0 => (front_dx.0, 0.34, false), 1 => (front_dx.1, 0.32, false), 2 => (-0.55, 0.3, false), 3 => (-0.15, 0.36, true), _ => (0.0, 0.36, true), }; let x0 = (cx + dx * span - pw * 0.5).round() as i32; let y0 = (top as f32 + dy * h).round() as i32; for (py, row) in pat.iter().enumerate() { for (px, ch) in row.chars().enumerate() { let col = match ch { 'k' => c(22, 16, 22), 'B' => c(150, 104, 58), 'b' => c(108, 70, 40), 'y' => c(232, 176, 48), 'Y' => c(255, 232, 120), 'd' => c(112, 74, 42), _ => continue, }; let (x, y) = (x0 + px as i32, y0 + py as i32); if x < 0 || y < 0 || x as usize >= n || y as usize >= n { continue; } let p = &mut g[y as usize][x as usize]; if front || p.is_none() { *p = Some(col); } } } } // writes a cell into an atlas of `cell` x `cell` texel cells: column, cell row (the white // silhouette goes into the row below), every cell pixel as k x k texels fn put_cell(img: &mut Image, col: u32, row: u32, g: &Cell, k: u32, cell: u32) { for (y, line) in g.iter().enumerate() { for (x, p) in line.iter().enumerate() { let Some(c) = p else { continue }; for d in 0..k * k { let (px, py) = (col * cell + x as u32 * k + d % k, row * cell + y as u32 * k + d / k); img.set_pixel(px, py, *c); img.set_pixel(px, py + cell, WHITE); } } } } // an n x n picture from a sheet into a cell grid of `size` (centred, the feet on the bottom row // of a 10x10 sprite: 11/12 of the cell) fn hd_cell(src: &Image, sx: u32, sy: u32, n: u32, size: u32) -> Cell { let mut g = vec![vec![None; size as usize]; size as usize]; let (ox, oy) = ((size - n) / 2, size * 11 / 12 - n); for y in 0..n { for x in 0..n { if sx + x < src.width as u32 && sy + y < src.height as u32 { let p = src.get_pixel(sx + x, sy + y); if p.a > 0.5 { g[(oy + y) as usize][(ox + x) as usize] = Some(Color::new(p.r, p.g, p.b, 1.0)); } } } } g } // The sprite atlas of the 10x10 sprites. Columns: SPRITE_KINDS, then SACK_KINDS with the sack. // Row (view * GEAR_TIERS + tier) * 2 with the gear of the tier, the row after it the white // silhouette (hit flash). fn gen_sprites() -> Image { let n = SPRITE_KINDS.len() as u32; let mut img = Image::gen_image_color(SPRITE_TEX_W as u16, SPRITE_TEX_H as u16, Color::new(0.0, 0.0, 0.0, 0.0)); let small = |k: EK, tier: u32, view: u32| -> Cell { sprite_with_gear(k, tier, view).iter().map(|r| r.to_vec()).collect() }; for view in 0..VIEWS { for tier in 0..GEAR_TIERS { let row = (view * GEAR_TIERS + tier) * 2; for (i, &k) in SPRITE_KINDS.iter().enumerate() { put_cell(&mut img, i as u32, row, &small(k, tier, view), SPR_K, ACELL); } for (i, &k) in SACK_KINDS.iter().enumerate() { let mut g = small(k, tier, view); add_sack(&mut g, view, &SACK_SMALL, (-0.5, -0.5)); put_cell(&mut img, n + i as u32, row, &g, SPR_K, ACELL); } } } img } // a picture into a texture of `cell` x `cell` cells, `cols` per row (no white silhouette: // the shader flashes the detailed sprites) fn put_picture(img: &mut Image, pic: u32, cols: u32, cell: u32, g: &Cell) { let (ox, oy) = ((pic % cols) * cell, (pic / cols) * cell); for (y, line) in g.iter().enumerate() { for (x, p) in line.iter().enumerate() { if let Some(c) = p { img.set_pixel(ox + x as u32, oy + y as u32, *c); } } } } // the two textures of the detailed sprites (64x64 level, 32x32 level); the gear anchors of every // picture are measured on the way fn gen_hd_sprites() -> (Image, Image) { let (w64, h64) = hd_tex_size(true); let (w32, h32) = hd_tex_size(false); let clear = Color::new(0.0, 0.0, 0.0, 0.0); let mut fine = Image::gen_image_color(w64 as u16, h64 as u16, clear); let mut coarse = Image::gen_image_color(w32 as u16, h32 as u16, clear); let mut anchors = vec![GearAnchor::default(); hd_pictures() as usize]; let png = |b: &[u8]| Image::from_file_with_format(b, Some(ImageFormat::Png)).expect("hd sprite"); for (j, hd) in HD_SPRITES.iter().enumerate() { let sheets64: Vec = std::iter::once(png(hd.idle)).chain(hd.anims.iter().map(|a| png(a.1))).collect(); let sheets32: Vec = std::iter::once(png(hd.idle_32)).chain(hd.anims.iter().map(|a| png(a.2))).collect(); let sacks = hd_sacks(j); for frame in 0..hd_frames(j) { // the frame's sheet and its place there: idle (frame 0) or frame k of an animation let (sheet, fx) = if frame == 0 { (0, None) } else { (1 + ((frame - 1) / ANIM_FRAMES) as usize, Some((frame - 1) % ANIM_FRAMES)) }; for view in 0..VIEWS { let at = |n: u32| match fx { None => (view * n, 0), Some(f) => (f * n, view * n), }; let (x64, y64) = at(64); let (x32, y32) = at(32); let g64 = hd_cell(&sheets64[sheet], x64, y64, 64, HD_CELL); let g32 = hd_cell(&sheets32[sheet], x32, y32, 32, HD_CELL / 2); for sack in 0..sacks { let pic = hd_base(j) + (frame * sacks + sack) * VIEWS + view; let (mut a, mut b) = (g64.clone(), g32.clone()); if sack == 1 { add_sack(&mut a, view, &SACK_64, (0.7, -0.65)); add_sack(&mut b, view, &SACK_32, (0.7, -0.65)); } anchors[pic as usize] = gear_anchor(&g64); put_picture(&mut fine, pic, HD_COLS64, HD_CELL, &a); put_picture(&mut coarse, pic, HD_COLS32, HD_CELL / 2, &b); } } } } let _ = HD_ANCHORS.set(anchors); (fine, coarse) } // the gear parts (see GEAR_HELMET etc.): grey with dark outlines, tinted at draw time fn gen_gear() -> Image { let mut img = Image::gen_image_color(GEAR_TEX as u16, GEAR_TEX as u16, Color::new(0.0, 0.0, 0.0, 0.0)); let grey = |v: f32| Color::new(v, v, v, 1.0); let outline = Color::new(0.08, 0.07, 0.09, 1.0); // a shape given by an inside test, outlined, lit from the upper left let mut shape = |(ox, oy, w, h): (u32, u32, u32, u32), inside: &dyn Fn(f32, f32) -> bool, shade: &dyn Fn(f32, f32) -> f32| { for y in 0..h { for x in 0..w { let (fx, fy) = ((x as f32 + 0.5) / w as f32, (y as f32 + 0.5) / h as f32); if !inside(fx, fy) { continue; } let edge = [(-1i32, 0i32), (1, 0), (0, -1), (0, 1)].iter().any(|&(dx, dy)| { let (nx, ny) = (x as i32 + dx, y as i32 + dy); nx < 0 || ny < 0 || nx >= w as i32 || ny >= h as i32 || !inside((nx as f32 + 0.5) / w as f32, (ny as f32 + 0.5) / h as f32) }); img.set_pixel(ox + x, oy + y, if edge { outline } else { grey(shade(fx, fy)) }); } } }; // helmet: a dome with a rim shape(GEAR_HELMET, &|x, y| (x - 0.5).powi(2) * 4.0 + (1.0 - y).powi(2) <= 1.0, &|x, y| if y > 0.75 { 0.55 } else { 0.95 - (x * 0.35 + y * 0.25) }); // shoulder piece: a rounded plate with a rivet shape(GEAR_PAULDRON, &|x, y| (x - 0.5).powi(2) * 4.0 + (y - 0.6).powi(2) * 2.6 <= 1.0, &|x, y| if (x - 0.5).abs() < 0.1 && (y - 0.5).abs() < 0.12 { 1.0 } else { 0.9 - y * 0.4 }); // crown: a band with three spikes shape(GEAR_CROWN, &|x, y| y > 0.55 || ((x * 3.0).fract() - 0.5).abs() < y * 0.55, &|x, y| if y > 0.6 && (x * 5.0).fract() < 0.25 { 0.6 } else { 1.0 - x * 0.2 }); // cape: wider at the hem, with folds shape(GEAR_CAPE, &|x, y| (x - 0.5).abs() < 0.32 + 0.18 * y, &|x, y| 0.8 - (((x - 0.5) * 9.0 + y).sin().abs()) * 0.25 - y * 0.15); // horn: a curved spike (bone) shape(GEAR_HORN, &|x, y| (x - (1.0 - y) * 0.4 - 0.3).abs() < 0.16 + 0.3 * y, &|x, y| 0.95 - y * 0.25 - x * 0.1); // plume: a feather bush shape(GEAR_PLUME, &|x, y| (x - 0.5).powi(2) * 3.2 + (y - 0.5).powi(2) * 1.1 <= 0.26 + 0.04 * (y * 14.0).sin(), &|_x, y| 0.95 - y * 0.3); 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.....", "................", ]; // the hand tool: an open goblin hand (four fingers, the thumb at the side), lit fingertips const CUR_OPEN: [&str; 16] = [ "...kk.kk.kk.....", "..klkklkklk.kk..", "..kgkkgkkgk.klk.", "..kgkkgkkgkkgk..", "..kgkkgkkgkkgk..", "..kggggggggggk..", "kk.kgggggggggk..", "klkkggggggggGk..", "kggkggggggggGk..", ".kgggggggggGGk..", "..kggggggggGk...", "...kgggggggGk...", "....kggggGGk....", ".....kkkkkk.....", "................", "................", ]; // holding units: a fist, knuckles on top, the thumb across const CUR_FIST: [&str; 16] = [ "................", "................", "....kkkkkkk.....", "...klklklklk....", "..kgkgkgkgGk....", "..kgkgkgkgGGk...", ".kkkkkkkggGGk...", "kllllllkgGGk....", "kggggggkgGGk....", ".kkkkkkggGGk....", "..kgggggggGk....", "...kggggggGk....", "....kGGGGGk.....", ".....kkkkk......", "................", "................", ]; fn cursor_pal(ch: char) -> Option { Some(match ch { 'k' => c(20, 16, 18), 'g' => c(112, 152, 84), 'G' => c(70, 100, 56), 'l' => c(168, 206, 124), '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); let badges = Texture2D::from_image(&crate::badges::gen_badges()); badges.set_filter(FilterMode::Nearest); let atlas_hd = Texture2D::from_image(&gen_hd_atlas(HD_TILE)); atlas_hd.set_filter(FilterMode::Nearest); let atlas_hd2 = Texture2D::from_image(&gen_hd_atlas(HD_TILE2)); atlas_hd2.set_filter(FilterMode::Nearest); let (hd64, hd32) = gen_hd_sprites(); let hd_sprites = Texture2D::from_image(&hd64); hd_sprites.set_filter(FilterMode::Nearest); let hd_sprites32 = Texture2D::from_image(&hd32); hd_sprites32.set_filter(FilterMode::Nearest); let gear = Texture2D::from_image(&gen_gear()); gear.set_filter(FilterMode::Nearest); let mat = |b: &[u8]| { let t = Texture2D::from_file_with_format(b, Some(ImageFormat::Png)); t.set_filter(FilterMode::Nearest); t }; let materials = MATERIALS.map(|(hd, _)| mat(hd)); let materials_px = MATERIALS.map(|(_, px)| mat(px)); Gfx { atlas, sprites, hd_sprites, hd_sprites32, gear, atlas_hd, atlas_hd2, materials, materials_px, cursors, badges, 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); let k = SPR_K as f32; Rect::new((i * ACELL) as f32 + k, (if white { ACELL } else { 0 }) as f32 + k, SPR as f32 * k, SPR as f32 * k) } // 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 * ACELL) as f32, (row * ACELL) as f32, ACELL as f32, ACELL as f32) } // the cell of a detailed sprite's view, if the kind has one (same layout as sprite_cell_view) // the picture of a detailed sprite, if the kind has one: a view, standing or a frame of an // animation (an animation the kind does not have: standing), with or without the sack pub fn hd_picture(k: EK, view: u32, anim: Option<(HdAnim, u32)>, sack: bool) -> Option { let j = HD_SPRITES.iter().position(|h| h.kind == k)?; let frame = anim .and_then(|(a, f)| HD_SPRITES[j].anims.iter().position(|x| x.0 == a).map(|i| 1 + i as u32 * ANIM_FRAMES + f % ANIM_FRAMES)) .unwrap_or(0); let sacks = hd_sacks(j); let sack = if sacks > 1 { sack as u32 } else { 0 }; Some(hd_base(j) + (frame * sacks + sack) * VIEWS + view.min(VIEWS - 1)) } // whether a kind has a drawn animation pub fn hd_has_anim(k: EK, anim: HdAnim) -> bool { HD_SPRITES.iter().any(|h| h.kind == k && h.anims.iter().any(|a| a.0 == anim)) } // a picture's place in its texture as texture coordinates (fine: hd_sprites, else hd_sprites32) pub fn hd_uv(pic: u32, fine: bool) -> Rect { let (cols, cell) = if fine { (HD_COLS64, HD_CELL) } else { (HD_COLS32, HD_CELL / 2) }; let (w, h) = hd_tex_size(fine); let (x, y) = ((pic % cols) * cell, (pic / cols) * cell); Rect::new(x as f32 / w as f32, y as f32 / h as f32, cell as f32 / w as f32, cell as f32 / h as f32) } // where the gear of a picture goes (measured when the textures were made) pub fn hd_anchor(pic: u32) -> GearAnchor { HD_ANCHORS.get().and_then(|a| a.get(pic as usize).copied()).unwrap_or_default() } // whether a kind carries its treasure in a sack on the back pub fn has_sack(k: EK) -> bool { SACK_KINDS.contains(&k) } // the cell of a 10x10 sprite with the sack on the back (only SACK_KINDS) pub fn sprite_cell_sack(k: EK, white: bool, level: u32, view: u32) -> Option { let i = SACK_KINDS.iter().position(|&s| s == k)? as u32; let r = Gfx::sprite_cell_view(k, white, level, view); Some(Rect::new(((SPRITE_KINDS.len() as u32 + i) * ACELL) as f32, r.y, r.w, r.h)) } 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 | Kind::Drawbridge => T_BRIDGE + (v & 1), Kind::Obsidian => T_OBSIDIAN + v, Kind::Mushroom => T_FLOOR + v, Kind::Tree => T_GRASS + v, Kind::Grove => T_MOSS + v, 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 } } } } // an atlas cell in the interface (tool and room buttons); the high-res style shows the // high-res atlas, so the room buttons show the rooms' new floors pub fn draw_cell(&self, id: u32, x: f32, y: f32, w: f32, h: f32, color: Color) { let (tex, k) = if HIGH_RES_UI.with(|h| h.get()) { (&self.atlas_hd, (HD_TILE / 16) as f32) } else { (&self.atlas, 1.0) }; let r = Self::cell(id); draw_texture_ex( tex, x, y, color, DrawTextureParams { dest_size: Some(vec2(w, h)), source: Some(Rect::new(r.x * k, r.y * k, r.w * k, r.h * k)), ..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 hd_atlas_tests { use super::*; #[test] fn high_res_atlas() { let small = gen_atlas(); for tile in [HD_TILE, HD_TILE2] { let hd = gen_hd_atlas(tile); let k = tile / 16; assert_eq!((hd.width as u32, hd.height as u32), (ATLAS_COLS * tile, ATLAS_ROWS * tile)); let px = |img: &Image, x: u32, y: u32| { let p = img.get_pixel(x, y); (p.r, p.g, p.b, p.a) }; // the colours are matched to the 16x16 cells (see match_colors), but a picture keeps its // shapes: what is lighter in the source stays lighter in the atlas let placed = |hd: &Image, ox: u32, oy: u32, src: &Image, sx: u32, sy: u32| { let lum = |p: Color| p.r + p.g + p.b; let pts = [(3, 5), (7, 40), (30, 9), (50, 50), (20, 60), (61, 2)]; for &(ax, ay) in pts.iter() { for &(bx, by) in pts.iter() { let (s1, s2) = (lum(src.get_pixel(sx + ax, sy + ay)), lum(src.get_pixel(sx + bx, sy + by))); if s1 + 0.15 < s2 { assert!(lum(hd.get_pixel(ox + ax, oy + ay)) <= lum(hd.get_pixel(ox + bx, oy + by)) + 0.02, "cell at {ox},{oy}"); } } } }; // a cell without an own texture: the 16x16 one scaled up let (cx, cy) = ((T_PUMPKIN % ATLAS_COLS) * 16, (T_PUMPKIN / ATLAS_COLS) * 16); for (x, y) in [(0, 0), (5, 9), (15, 15)] { assert_eq!(px(&hd, (cx + x) * k + 2, (cy + y) * k + 1), px(&small, cx + x, cy + y)); } // the generated ones: the quarters of their 2x2 sheet for &(base, n, png, png2) in HD_TILES.iter() { let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).unwrap(); let side = if n == 4 { 2 * tile } else { tile }; assert_eq!((src.width as u32, src.height as u32), (side, side)); for v in 0..n { let id = base + v; let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile); let (sx, sy) = if n == 4 { ((v % 2) * tile, (v / 2) * tile) } else { (0, 0) }; placed(&hd, ox, oy, &src, sx, sy); } } // the sets: five tiles each in their own cells (inside the atlas) for &(_, _, own, png, png2, _) in HD_SETS.iter() { let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).unwrap(); assert_eq!((src.width as u32, src.height as u32), (5 * tile, tile)); assert!(own + 5 <= ATLAS_COLS * ATLAS_ROWS); for v in 0..5 { let id = own + v; let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile); placed(&hd, ox, oy, &src, v * tile, 0); } } } // the icons: a strip with one icon per cell for &(png, png2, cells) in HD_ICONS.iter() { for tile in [HD_TILE, HD_TILE2] { let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).unwrap(); assert_eq!((src.width as u32, src.height as u32), (cells.len() as u32 * tile, tile)); } } // the variants: as many tiles as the kind has cells, every cell filled for &(base, n, png, png2) in HD_VARS.iter() { for tile in [HD_TILE, HD_TILE2] { let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).unwrap(); assert_eq!((src.width as u32, src.height as u32), (n * tile, tile), "cell {base}"); } for z in 0..8 { for x in 0..8 { assert!((base..base + n).contains(&hd_tile(base, x, z))); } } } // the decals: transparent around their pictures for &(id, _, _) in HD_DECALS.iter() { for tile in [HD_TILE, HD_TILE2] { let hd = gen_hd_atlas(tile); let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile); let clear = (0..tile * tile).filter(|i| hd.get_pixel(ox + i % tile, oy + i / tile).a < 0.5).count() as u32; assert!(clear > tile * tile / 3 && clear < tile * tile, "decal {id} at {tile}: {clear} clear texels"); } } // every tile of a set shows up let mut n = [0u32; 5]; for z in 0..60 { for x in 0..60 { let id = hd_tile(T_CLAIM + (x as u32 % 4), x, z); assert!((T_HDSET..T_HDSET + 5).contains(&id)); n[(id - T_HDSET) as usize] += 1; } } assert!(n.iter().all(|&k| k > 100), "{n:?}"); assert!((T_HDSET + 15..T_HDSET + 20).contains(&hd_tile(T_RIVAL3 + 2, 3, 7))); assert!((T_HDSET + 50..T_HDSET + 55).contains(&hd_tile(T_DIAMOND + 1, 8, 2))); assert!((T_HDSET + 25..T_HDSET + 30).contains(&hd_tile(T_EARTH + 3, 5, 5))); // the liquids: four variants by position; others stay assert_eq!(hd_tile(T_WATER + 3, 0, 0), T_WATER); assert_eq!(hd_tile(T_LAVA, 1, 1), T_LAVA + 3); assert_eq!(hd_tile(T_PUMPKIN + 2, 1, 0), T_PUMPKIN + 2); } } #[cfg(test)] mod atlas_dump { use super::*; // `cargo test --bins dump_atlas_cell -- --ignored` writes a 16x16 cell kind (env DUMP_CELL, // default T_REINF: its four variants at random, 6x4 tiles) scaled up 8x to /tmp/atlas-cell.png #[test] #[ignore] fn dump_atlas_cell() { let base: u32 = std::env::var("DUMP_CELL").ok().and_then(|v| v.parse().ok()).unwrap_or(T_REINF); let atlas = gen_atlas(); let (w, h) = (6 * 16 * 8, 4 * 16 * 8); let mut out = Image::gen_image_color(w as u16, h as u16, BLACK); for t in 0..24u32 { let id = base + hash(t as i32, 3, 5) % 4; let (ox, oy) = ((id % ATLAS_COLS) * 16, (id / ATLAS_COLS) * 16); for y in 0..128 { for x in 0..128 { out.set_pixel((t % 6) * 128 + x, h - 1 - ((t / 6) * 128 + y), atlas.get_pixel(ox + x / 8, oy + y / 8)); } } } out.export_png("/tmp/atlas-cell.png"); } // `cargo test --bins dump_atlases -- --ignored`: the 16x16 atlas scaled up and the 64x64 // high-res atlas side by side in /tmp/atlases.png (to compare the colours of the styles) #[test] #[ignore] fn dump_atlases() { let small = gen_atlas(); let hd = gen_hd_atlas(HD_TILE); let (w, h) = (hd.width as u32, hd.height as u32); let mut out = Image::gen_image_color((2 * w + 16) as u16, h as u16, BLACK); for y in 0..h { for x in 0..w { out.set_pixel(x, h - 1 - y, small.get_pixel(x / 4, y / 4)); out.set_pixel(w + 16 + x, h - 1 - y, hd.get_pixel(x, y)); } } out.export_png("/tmp/atlases.png"); } } #[cfg(test)] mod sprite_tests { use super::*; // texels that differ between two cells fn differ(img: &Image, a: Rect, b: Rect) -> usize { let n = a.w as u32; (0..n * n) .filter(|i| { let p = img.get_pixel(a.x as u32 + i % n, a.y as u32 + i / n); let q = img.get_pixel(b.x as u32 + i % n, b.y as u32 + i / n); (p.r, p.g, p.b, p.a) != (q.r, q.g, q.b, q.a) }) .count() } // texels of a picture that differ with and without something (pictures of the 64x64 level) fn pic_texels(img: &Image, pic: u32) -> Vec<(u8, u8, u8, u8)> { let (ox, oy) = ((pic % HD_COLS64) * HD_CELL, (pic / HD_COLS64) * HD_CELL); (0..HD_CELL * HD_CELL) .map(|i| { let p = img.get_pixel(ox + i % HD_CELL, oy + i / HD_CELL); ((p.r * 255.0) as u8, (p.g * 255.0) as u8, (p.b * 255.0) as u8, (p.a * 255.0) as u8) }) .collect() } #[test] fn detailed_sprites() { let (fine, coarse) = gen_hd_sprites(); assert_eq!((fine.width as u32, fine.height as u32), hd_tex_size(true)); assert_eq!((coarse.width as u32, coarse.height as u32), hd_tex_size(false)); assert!(fine.width <= 4096 && fine.height <= 4096, "the texture fits every GPU"); // the imp: idle and the digging frames, with and without the sack for view in 0..VIEWS { for anim in [None, Some((HdAnim::Hack, 0)), Some((HdAnim::Hack, 3))] { let p = Gfx::hd_picture(EK::Worker, view, anim, false).unwrap(); let s = Gfx::hd_picture(EK::Worker, view, anim, true).unwrap(); let (a, b) = (pic_texels(&fine, p), pic_texels(&fine, s)); assert!(a.iter().filter(|t| t.3 > 128).count() > 600, "view {view} {anim:?} drawn"); assert!(a.iter().zip(b.iter()).filter(|(x, y)| x != y).count() > 60, "view {view} {anim:?}: the sack shows"); } } assert!(Gfx::hd_has_anim(EK::Worker, HdAnim::Hack)); // every kind has its pictures; an animation it lacks shows it standing let mut seen = std::collections::HashSet::new(); for &k in SPRITE_KINDS.iter() { for view in 0..VIEWS { let p = Gfx::hd_picture(k, view, None, false).expect("a detailed sprite for every kind"); assert!(seen.insert(p), "{k:?}: pictures do not overlap"); assert!(pic_texels(&fine, p).iter().filter(|t| t.3 > 128).count() > 100, "{k:?} view {view}"); let a = Gfx::hd_anchor(p); assert!(a.top < a.shoulder && a.shoulder < a.bottom && a.sh.0 < a.sh.1, "{k:?} view {view}: {a:?}"); if !Gfx::hd_has_anim(k, HdAnim::Walk) { assert_eq!(Gfx::hd_picture(k, view, Some((HdAnim::Walk, 2)), false), Some(p)); } let uv = Gfx::hd_uv(p, false); assert!(uv.x + uv.w <= 1.0 + 1e-6 && uv.y + uv.h <= 1.0 + 1e-6); } } } #[test] fn small_sprites_and_sack() { let img = gen_sprites(); assert_eq!((img.width as f32, img.height as f32), (SPRITE_TEX_W, SPRITE_TEX_H)); // every pixel a 3x3 block let r = Gfx::sprite_cell_view(EK::Knight, false, 1, 0); for y in 0..SPR_CELL { for x in 0..SPR_CELL { let p = img.get_pixel(r.x as u32 + x * SPR_K, r.y as u32 + y * SPR_K); let q = img.get_pixel(r.x as u32 + x * SPR_K + 2, r.y as u32 + y * SPR_K + 2); assert_eq!((p.r, p.g, p.b, p.a), (q.r, q.g, q.b, q.a)); } } assert!(Gfx::sprite_cell_sack(EK::Knight, false, 1, 0).is_none()); for view in 0..VIEWS { let plain = Gfx::sprite_cell_view(EK::Worker, false, 1, view); let sack = Gfx::sprite_cell_sack(EK::Worker, false, 1, view).unwrap(); assert!(differ(&img, sack, plain) > 30, "view {view}: the small sack shows"); } } // `cargo test --bins dump_imp_cells -- --ignored` writes the imp's detailed pictures (idle // and the digging frames, without and with sack; the 64x64 level) and the gear parts to /tmp #[test] #[ignore] fn dump_imp_cells() { let (fine, _) = gen_hd_sprites(); let mut rows: Vec> = Vec::new(); for sack in [false, true] { for anim in [None, Some((HdAnim::Hack, 0)), Some((HdAnim::Hack, 1)), Some((HdAnim::Hack, 2)), Some((HdAnim::Hack, 3))] { rows.push((0..VIEWS).map(|v| Gfx::hd_picture(EK::Worker, v, anim, sack).unwrap()).collect()); } } let c = HD_CELL; let (w, h) = (c * VIEWS, c * rows.len() as u32); let mut out = Image::gen_image_color(w as u16, h as u16, Color::new(0.35, 0.33, 0.38, 1.0)); for (row, list) in rows.iter().enumerate() { for (i, &pic) in list.iter().enumerate() { let (ox, oy) = ((pic % HD_COLS64) * c, (pic / HD_COLS64) * c); for y in 0..c { for x in 0..c { let p = fine.get_pixel(ox + x, oy + y); if p.a > 0.5 { out.set_pixel(i as u32 * c + x, h - 1 - (row as u32 * c + y), p); } } } } } out.export_png("/tmp/imp-cells.png"); let gear = gen_gear(); let mut g = Image::gen_image_color((GEAR_TEX * 4) as u16, (GEAR_TEX * 4) as u16, Color::new(0.35, 0.33, 0.38, 1.0)); for y in 0..GEAR_TEX * 4 { for x in 0..GEAR_TEX * 4 { let p = gear.get_pixel(x / 4, y / 4); if p.a > 0.5 { g.set_pixel(x, GEAR_TEX * 4 - 1 - y, p); } } } g.export_png("/tmp/gear.png"); } } #[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), 3, &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 s in crate::spells::SPELLS.iter() { save(&atlas, Gfx::cell(crate::spells::spell_icon(*s)), 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); } } }