2528 lines
92 KiB
Rust
2528 lines
92 KiB
Rust
// Procedurally generated pixel art: tile atlas, unit sprites and the bitmap font.
|
||
|
||
use crate::game::EK;
|
||
use crate::map::{Kind, Owner};
|
||
use macroquad::prelude::*;
|
||
|
||
pub const CELL: f32 = 16.0;
|
||
const ATLAS_COLS: u32 = 16;
|
||
pub const ATLAS_ROWS: u32 = 14;
|
||
|
||
// atlas indices (4 variants each)
|
||
pub const T_ROCK: u32 = 0;
|
||
pub const T_EARTH: u32 = 4;
|
||
pub const T_GOLD: u32 = 8;
|
||
pub const T_FLOOR: u32 = 12;
|
||
pub const T_CLAIM: u32 = 16;
|
||
pub const T_HEROFLOOR: u32 = 20;
|
||
pub const T_TREASURY: u32 = 24;
|
||
pub const T_LAIR: u32 = 28;
|
||
pub const T_FARM: u32 = 32;
|
||
pub const T_TRAINING: u32 = 36;
|
||
pub const T_LIBRARY: u32 = 40;
|
||
pub const T_COREFLOOR: u32 = 44;
|
||
pub const T_PORTALFLOOR: u32 = 48;
|
||
pub const T_GATEFLOOR: u32 = 52;
|
||
// single cells
|
||
pub const T_FACE_EARTH: u32 = 56;
|
||
pub const T_FACE_ROCK: u32 = 57;
|
||
pub const T_FACE_GOLD: u32 = 58;
|
||
pub const T_MARK: u32 = 59;
|
||
pub const D_BED: u32 = 60;
|
||
pub const D_MUSHROOM: u32 = 61;
|
||
pub const D_DUMMY: u32 = 62;
|
||
pub const D_COINS: u32 = 63;
|
||
pub const D_SHELF: u32 = 64;
|
||
pub const I_PICK: u32 = 65;
|
||
pub const I_HAND: u32 = 66;
|
||
pub const I_SELL: u32 = 67;
|
||
pub const I_FLAG: u32 = 68;
|
||
pub const I_BOLT: u32 = 69;
|
||
pub const I_HEAL: u32 = 70;
|
||
pub const I_SUMMON: u32 = 71;
|
||
pub const T_REINF: u32 = 72; // 4 variants: reinforced wall masonry
|
||
pub const T_TORTURE: u32 = 76; // 4 variants: torture chamber floor
|
||
pub const D_RACK: u32 = 80;
|
||
pub const I_ARROW: u32 = 81;
|
||
pub const I_CANNON: u32 = 82;
|
||
pub const I_MAGIC: u32 = 83;
|
||
pub const T_TEMPLE: u32 = 84; // 4 variants
|
||
pub const D_ALTAR: u32 = 88;
|
||
pub const T_WATER: u32 = 96; // 4 variants
|
||
pub const T_LAVA: u32 = 100; // 4 variants
|
||
pub const T_BRIDGE: u32 = 104; // 2 variants: wooden planks
|
||
pub const T_STONEBRIDGE: u32 = 106; // 2 variants: stone slabs (over lava)
|
||
pub const T_FORTWALL: u32 = 108; // 4 variants: knight fortress masonry
|
||
pub const D_SHARD: u32 = 112; // shard of the soul core
|
||
pub const D_BANNER: u32 = 113; // knight banner
|
||
pub const I_BRIDGE: u32 = 114;
|
||
pub const T_SILVER: u32 = 115; // 2 variants each: veins in the earth
|
||
pub const T_RUBY: u32 = 117;
|
||
pub const T_DIAMOND: u32 = 119;
|
||
pub const D_SILVER: u32 = 121; // treasury content
|
||
pub const D_RUBY: u32 = 122;
|
||
pub const D_DIAMOND: u32 = 123;
|
||
pub const D_RUBBLE: u32 = 124; // floor decals: pebbles and fragments
|
||
pub const D_CRACK: u32 = 125; // a long crack through several stones
|
||
pub const T_AERIE: u32 = 126; // 4 variants: straw, twigs and feathers
|
||
pub const T_STEEL: u32 = 130; // 4 variants: riveted iron plates
|
||
pub const I_STEEL: u32 = 134;
|
||
pub const T_RIVAL1: u32 = 135; // 4 variants: floor of the green master
|
||
pub const T_RIVAL2: u32 = 139; // 4 variants: floor of the blue master
|
||
pub const I_RUNE: u32 = 143; // rune forge tower
|
||
pub const T_FORGE: u32 = 144; // 4 variants: sooty flagstones with glowing cracks
|
||
pub const I_TRAPS: u32 = 148; // 10 icons in TRAP_KINDS order
|
||
pub const I_SPELLS: u32 = 158; // 8 icons in SPELLS order
|
||
pub const T_PRISON: u32 = 166; // 4 variants each
|
||
pub const T_GRAVEYARD: u32 = 170;
|
||
pub const T_GUARDPOST: u32 = 174;
|
||
pub const T_ARENA: u32 = 178;
|
||
pub const T_ALTAR: u32 = 182;
|
||
pub const I_ORDERS: u32 = 186; // attack, guard
|
||
pub const T_RIVAL3: u32 = 189; // 4 variants: floor of the yellow master
|
||
pub 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 struct Gfx {
|
||
pub atlas: Texture2D,
|
||
pub sprites: Texture2D,
|
||
pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*)
|
||
pub badges: Texture2D, // achievement badges, 32x32 each (see badges.rs)
|
||
pub font: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE
|
||
pub font_map: FontMap, // which glyphs the two fonts hold
|
||
}
|
||
|
||
// ------------------------------------------------------------------ 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<usize> {
|
||
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<u8> {
|
||
// 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<char> {
|
||
Some(match ch {
|
||
'✓' | '✔' => '√',
|
||
'ẞ' => 'ß',
|
||
'–' | '—' => '-',
|
||
'„' | '“' | '”' => '"',
|
||
'‚' | '‘' | '’' => '\'',
|
||
'…' => '.',
|
||
_ => return None,
|
||
})
|
||
}
|
||
|
||
// which glyphs the two sheets hold
|
||
pub struct FontMap {
|
||
has: Vec<bool>, // 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<Color>) {
|
||
let ox = (id % ATLAS_COLS) * 16;
|
||
let oy = (id / ATLAS_COLS) * 16;
|
||
for y in 0..16 {
|
||
for x in 0..16 {
|
||
if let Some(col) = f(x, y) {
|
||
self.img.set_pixel(ox + x as u32, oy + y as u32, col);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// flagstone pattern with grout; stones are whole, cracked or broken (chipped corner with grit)
|
||
fn stones(x: i32, y: i32, v: u32, base: (i32, i32, i32), grout: (i32, i32, i32), var: i32) -> Color {
|
||
let row = y / 8;
|
||
let off = if row % 2 == 1 { 4 } else { 0 };
|
||
let sx = (x + off) % 16;
|
||
if y % 8 == 0 || sx % 8 == 0 {
|
||
return shade(grout, (rnd(x, y, v + 7) * 6.0) as i32);
|
||
}
|
||
let (lx, ly) = (sx % 8, y % 8); // position inside the stone (1..7)
|
||
let damage = hash((x + off) / 8, row, v + 17) % 9;
|
||
match damage {
|
||
// a crack running across the stone
|
||
0 | 1 => {
|
||
let k = (hash((x + off) / 8, row, v + 18) % 3) as i32;
|
||
let on = if damage == 0 { lx + k == ly || lx + k == ly + 1 && ly % 3 == 0 } else { 8 - lx == ly + k - 1 };
|
||
if on {
|
||
return shade(grout, -6);
|
||
}
|
||
}
|
||
// broken corner: the piece is missing, grit lies in the hole
|
||
2 => {
|
||
if lx + ly < 5 {
|
||
return if hash(x, y, v + 19) % 3 == 0 { shade(base, -18) } else { shade(grout, (rnd(x, y, v + 20) * 10.0) as i32 - 4) };
|
||
}
|
||
}
|
||
// shattered: several small fragments
|
||
3 => {
|
||
if (lx * 3 + ly * 5 + v as i32) % 7 == 0 || lx == ly / 2 + 2 {
|
||
return shade(grout, 2);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
let stone = hash((x + off) / 8, row, v + 3) % 5;
|
||
let d = (rnd(x, y, v) * var as f32) as i32 - var / 2 + stone as i32 * 3 - 6;
|
||
// highlight top/left, shadow bottom/right
|
||
let edge = if y % 8 == 1 || sx % 8 == 1 {
|
||
10
|
||
} else if y % 8 == 7 || sx % 8 == 7 {
|
||
-8
|
||
} else {
|
||
0
|
||
};
|
||
shade(base, d + edge)
|
||
}
|
||
|
||
fn earth_px(x: i32, y: i32, v: u32) -> Color {
|
||
let base = (98, 68, 44);
|
||
let big = rnd(x / 3, y / 3, v + 11) * 16.0 - 8.0;
|
||
let fine = rnd(x, y, v) * 14.0 - 7.0;
|
||
if hash(x, y, v + 99) % 37 == 0 {
|
||
return c(150, 120, 90);
|
||
}
|
||
if hash(x, y, v + 98) % 29 == 0 {
|
||
return c(62, 42, 28);
|
||
}
|
||
shade(base, (big + fine) as i32)
|
||
}
|
||
|
||
fn gen_atlas() -> Image {
|
||
let mut cv = Canvas { img: Image::gen_image_color((ATLAS_COLS * 16) as u16, (ATLAS_ROWS * 16) as u16, Color::new(0.0, 0.0, 0.0, 0.0)) };
|
||
for v in 0..4u32 {
|
||
// rock
|
||
cv.cell(T_ROCK + v, |x, y| {
|
||
let blk = rnd(x / 4 + (y / 4) % 2, y / 4, v + 21) * 20.0 - 10.0;
|
||
let fine = rnd(x, y, v + 5) * 10.0 - 5.0;
|
||
let crack = (x + y * 3 + v as i32 * 5) % 23 == 0 || (x * 2 - y + v as i32) % 31 == 0;
|
||
if crack {
|
||
Some(c(40, 40, 48))
|
||
} else {
|
||
Some(shade((80, 80, 92), (blk + fine) as i32))
|
||
}
|
||
});
|
||
// earth
|
||
cv.cell(T_EARTH + v, |x, y| Some(earth_px(x, y, v)));
|
||
// gold
|
||
cv.cell(T_GOLD + v, |x, y| {
|
||
let mut best = 99.0f32;
|
||
for k in 0..6 {
|
||
let nx = 2 + (hash(k, 1, v + 40) % 12) as i32;
|
||
let ny = 2 + (hash(k, 2, v + 40) % 12) as i32;
|
||
let d = (((x - nx) * (x - nx) + (y - ny) * (y - ny)) as f32).sqrt();
|
||
best = best.min(d - if k % 2 == 0 { 0.5 } else { 0.0 });
|
||
}
|
||
if best < 0.6 {
|
||
Some(c(255, 240, 150))
|
||
} else if best < 1.4 {
|
||
Some(c(240, 196, 52))
|
||
} else if best < 2.0 {
|
||
Some(c(168, 118, 30))
|
||
} else {
|
||
Some(earth_px(x, y, v + 60))
|
||
}
|
||
});
|
||
cv.cell(T_FLOOR + v, |x, y| Some(stones(x, y, v, (64, 56, 50), (34, 30, 28), 10)));
|
||
cv.cell(T_CLAIM + v, |x, y| {
|
||
let col = stones(x, y, v + 10, (78, 44, 58), (36, 20, 28), 10);
|
||
// small emblem in the middle of every other stone
|
||
if (x % 8 == 4 && y % 8 == 4) && hash(x / 8, y / 8, v) % 2 == 0 {
|
||
Some(c(150, 60, 80))
|
||
} else {
|
||
Some(col)
|
||
}
|
||
});
|
||
cv.cell(T_HEROFLOOR + v, |x, y| Some(stones(x, y, v + 20, (132, 136, 150), (80, 84, 96), 12)));
|
||
cv.cell(T_TREASURY + v, |x, y| {
|
||
let chk = ((x / 4) + (y / 4)) % 2 == 0;
|
||
let base = if chk { (120, 92, 36) } else { (100, 74, 30) };
|
||
let d = (rnd(x, y, v + 30) * 10.0) as i32 - 5;
|
||
if x % 4 == 0 || y % 4 == 0 {
|
||
Some(shade((70, 52, 22), d))
|
||
} else {
|
||
Some(shade(base, d))
|
||
}
|
||
});
|
||
cv.cell(T_LAIR + v, |x, y| {
|
||
let streak = ((x * 2 + y + (hash(y, 0, v) % 3) as i32) % 5) as i32;
|
||
let d = (rnd(x, y, v + 31) * 18.0) as i32 - 9 + streak * 4;
|
||
Some(shade((112, 86, 46), d))
|
||
});
|
||
cv.cell(T_FARM + v, |x, y| {
|
||
let moss = rnd(x / 2, y / 2, v + 32);
|
||
let d = (rnd(x, y, v + 33) * 14.0) as i32 - 7;
|
||
if moss > 0.55 {
|
||
Some(shade((58, 98, 40), d))
|
||
} else {
|
||
Some(shade((62, 46, 32), d))
|
||
}
|
||
});
|
||
cv.cell(T_TRAINING + v, |x, y| {
|
||
let plank = y / 4;
|
||
let off = (hash(plank, 0, v + 34) % 16) as i32;
|
||
if y % 4 == 0 || (x + off) % 16 == 0 {
|
||
Some(c(58, 38, 24))
|
||
} else {
|
||
let grain = ((x + plank * 5) % 7 == 0) as i32 * -10;
|
||
Some(shade((116, 80, 48), (rnd(x, y, v + 35) * 10.0) as i32 + grain))
|
||
}
|
||
});
|
||
cv.cell(T_LIBRARY + v, |x, y| {
|
||
if x >= 3 && x <= 12 && y >= 3 && y <= 12 {
|
||
let border = x == 3 || x == 12 || y == 3 || y == 12;
|
||
if border {
|
||
Some(c(170, 130, 50))
|
||
} else {
|
||
Some(shade((116, 30, 42), (rnd(x, y, v + 36) * 10.0) as i32))
|
||
}
|
||
} else {
|
||
let plank = x / 4;
|
||
if x % 4 == 0 {
|
||
Some(c(40, 24, 16))
|
||
} else {
|
||
Some(shade((74, 46, 32), (rnd(x, y, v + 37) * 10.0) as i32 + (plank % 2) * 4))
|
||
}
|
||
}
|
||
});
|
||
cv.cell(T_COREFLOOR + v, |x, y| Some(stones(x, y, v + 40, (86, 32, 34), (40, 14, 16), 10)));
|
||
cv.cell(T_PORTALFLOOR + v, |x, y| Some(stones(x, y, v + 50, (66, 44, 92), (32, 20, 46), 10)));
|
||
cv.cell(T_GATEFLOOR + v, |x, y| {
|
||
let vein = ((x as f32 * 0.7 + (y as f32 * 0.5).sin() * 3.0 + v as f32 * 4.0) as i32) % 9 == 0;
|
||
if vein {
|
||
Some(c(150, 150, 165))
|
||
} else {
|
||
Some(shade((196, 196, 206), (rnd(x, y, v + 60) * 12.0) as i32 - 6))
|
||
}
|
||
});
|
||
}
|
||
// wall faces
|
||
cv.cell(T_FACE_EARTH, |x, y| {
|
||
let streak = (hash(x, 0, 70) % 3) as i32 * 5;
|
||
Some(shade((62, 42, 28), (rnd(x, y, 71) * 10.0) as i32 - streak - y / 2))
|
||
});
|
||
cv.cell(T_FACE_ROCK, |x, y| {
|
||
let streak = (hash(x, 0, 72) % 3) as i32 * 5;
|
||
Some(shade((54, 54, 64), (rnd(x, y, 73) * 10.0) as i32 - streak - y / 2))
|
||
});
|
||
cv.cell(T_FACE_GOLD, |x, y| {
|
||
if hash(x, y, 74) % 9 == 0 {
|
||
Some(c(200, 160, 40))
|
||
} else {
|
||
Some(shade((62, 42, 28), (rnd(x, y, 75) * 10.0) as i32 - y / 2))
|
||
}
|
||
});
|
||
// dig marker: diagonal stripes
|
||
cv.cell(T_MARK, |x, y| {
|
||
if (x + y) % 6 < 2 {
|
||
Some(Color::from_rgba(255, 220, 60, 150))
|
||
} else if x == 0 || y == 0 || x == 15 || y == 15 {
|
||
Some(Color::from_rgba(255, 220, 60, 200))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
// bed
|
||
cv.cell(D_BED, |x, y| {
|
||
let dx = (x as f32 - 7.5) / 6.5;
|
||
let dy = (y as f32 - 8.0) / 5.5;
|
||
let d = dx * dx + dy * dy;
|
||
if d < 0.55 {
|
||
Some(if x < 5 { c(200, 190, 160) } else { c(110, 50, 60) })
|
||
} else if d < 1.0 {
|
||
Some(c(150, 118, 64))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
// mushrooms
|
||
cv.cell(D_MUSHROOM, |x, y| {
|
||
let caps = [(4, 6, 3), (11, 5, 2), (8, 11, 3)];
|
||
for &(cx, cy, r) in &caps {
|
||
let dx = x - cx;
|
||
let dy = y - cy;
|
||
if dy <= 0 && dx * dx + dy * dy * 2 <= r * r + 1 {
|
||
return Some(if (x + y) % 3 == 0 && dy < 0 { c(250, 230, 220) } else { c(200, 40, 50) });
|
||
}
|
||
if dx.abs() <= 0 && dy > 0 && dy <= r {
|
||
return Some(c(230, 220, 200));
|
||
}
|
||
}
|
||
None
|
||
});
|
||
// training dummy
|
||
cv.cell(D_DUMMY, |x, y| {
|
||
if (x == 7 || x == 8) && y >= 4 {
|
||
Some(c(90, 60, 36))
|
||
} else if y == 7 && (3..=12).contains(&x) {
|
||
Some(c(90, 60, 36))
|
||
} else if (x - 7).pow(2) + (y - 3).pow(2) <= 5 {
|
||
Some(c(200, 170, 110))
|
||
} else if y >= 14 && (4..=11).contains(&x) {
|
||
Some(c(60, 40, 24))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
// gold pile
|
||
cv.cell(D_COINS, |x, y| {
|
||
let dx = (x as f32 - 7.5) / 7.0;
|
||
let dy = (y as f32 - 11.0) / 5.0;
|
||
let d = dx * dx + dy * dy;
|
||
if d < 1.0 && y <= 14 {
|
||
let hl = hash(x, y, 80) % 5 == 0;
|
||
Some(if hl { c(255, 245, 170) } else if d > 0.6 { c(190, 140, 30) } else { c(242, 200, 60) })
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
// bookshelf
|
||
cv.cell(D_SHELF, |x, y| {
|
||
if y > 9 || x == 0 || x == 15 {
|
||
return if y <= 10 { Some(c(60, 36, 22)) } else { None };
|
||
}
|
||
if y == 0 || y == 5 {
|
||
return Some(c(60, 36, 22));
|
||
}
|
||
let book = hash(x, y / 5, 81) % 5;
|
||
Some(match book {
|
||
0 => c(150, 40, 40),
|
||
1 => c(40, 80, 140),
|
||
2 => c(60, 120, 60),
|
||
3 => c(160, 130, 50),
|
||
_ => c(100, 60, 120),
|
||
})
|
||
});
|
||
// reinforced walls: masonry in dark stone with the player's red mortar
|
||
for v in 0..4u32 {
|
||
cv.cell(T_REINF + v, |x, y| {
|
||
let row = y / 4;
|
||
let off = if row % 2 == 1 { 4 } else { 0 };
|
||
if y % 4 == 3 || (x + off) % 8 == 7 {
|
||
return Some(shade((70, 26, 30), (rnd(x, y, v + 90) * 8.0) as i32));
|
||
}
|
||
let brick = hash((x + off) / 8, row, v + 91) % 4;
|
||
Some(shade((84, 76, 74), (rnd(x, y, v + 92) * 14.0) as i32 - 7 + brick as i32 * 5 - 6 + if y % 4 == 0 { 10 } else { 0 }))
|
||
});
|
||
// torture chamber: dark stones with rusty iron grates
|
||
cv.cell(T_TORTURE + v, |x, y| {
|
||
let grate = (4..=11).contains(&x) && (4..=11).contains(&y) && (x % 3 == 1 || y % 3 == 1);
|
||
if grate {
|
||
return Some(shade((96, 44, 30), (rnd(x, y, v + 93) * 12.0) as i32));
|
||
}
|
||
Some(stones(x, y, v + 94, (52, 40, 44), (26, 16, 18), 10))
|
||
});
|
||
}
|
||
// temple: dark marble with a glowing red-gold rune circle
|
||
for v in 0..4u32 {
|
||
cv.cell(T_TEMPLE + v, |x, y| {
|
||
let (dx, dy) = (x as f32 - 7.5, y as f32 - 7.5);
|
||
let d = (dx * dx + dy * dy).sqrt();
|
||
if (5.0..6.0).contains(&d) {
|
||
return Some(c(170, 70, 40));
|
||
}
|
||
if d < 1.5 {
|
||
return Some(c(220, 150, 60));
|
||
}
|
||
let vein = ((x as f32 * 0.6 + (y as f32 * 0.7).sin() * 2.0 + v as f32 * 3.0) as i32) % 7 == 0;
|
||
Some(if vein { c(70, 54, 66) } else { shade((44, 34, 46), (rnd(x, y, v + 95) * 10.0) as i32) })
|
||
});
|
||
}
|
||
// altar with a flame
|
||
cv.cell(D_ALTAR, |x, y| {
|
||
if y >= 9 && (3..=12).contains(&x) {
|
||
Some(if y == 9 { c(150, 140, 150) } else { c(100, 92, 104) })
|
||
} else if y < 9 && y >= 3 && (x - 7).abs() + (y - 8).abs() / 2 <= 3 {
|
||
Some(if (x + y) % 3 == 0 { c(255, 220, 80) } else { c(255, 120, 30) })
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
// torture wheel
|
||
cv.cell(D_RACK, |x, y| {
|
||
let (dx, dy) = (x as f32 - 7.5, y as f32 - 7.0);
|
||
let d = (dx * dx + dy * dy).sqrt();
|
||
let a = dy.atan2(dx);
|
||
if (5.5..7.0).contains(&d) || (d < 6.0 && (a * 4.0 / std::f32::consts::PI).fract().abs() < 0.12) {
|
||
Some(c(110, 76, 44))
|
||
} else if y >= 13 && (x == 7 || x == 8 || (4..=11).contains(&x) && y == 15) {
|
||
Some(c(70, 48, 30))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
|
||
// tool icons
|
||
cv.cell(I_PICK, |x, y| {
|
||
let handle = (x + y == 16 || x + y == 17) && x >= 3;
|
||
let head = (y == 3 || y == 4) && (2..=13).contains(&x) && (x - 8).abs() + (y - 2) * 2 < 9;
|
||
if head {
|
||
Some(c(200, 205, 215))
|
||
} else if handle {
|
||
Some(c(140, 90, 50))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
cv.cell(I_HAND, |x, y| {
|
||
let palm = (3..=12).contains(&x) && (7..=14).contains(&y);
|
||
let fingers = y >= 2 && y < 8 && (x == 4 || x == 6 || x == 8 || x == 10) && !(x == 10 && y < 4);
|
||
let thumb = (x == 13 || x == 14) && (6..=10).contains(&y);
|
||
if palm || fingers || thumb {
|
||
Some(c(220, 180, 150))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
cv.cell(I_SELL, |x, y| {
|
||
let d = (x - 8) * (x - 8) + (y - 8) * (y - 8);
|
||
if d <= 36 && d >= 20 {
|
||
Some(c(240, 200, 60))
|
||
} else if (x == 8 && (4..=12).contains(&y)) || (y == 8 && (5..=11).contains(&x)) {
|
||
Some(c(240, 200, 60))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
cv.cell(I_FLAG, |x, y| {
|
||
if x == 3 && y >= 1 {
|
||
Some(c(180, 180, 180))
|
||
} else if x > 3 && x < 14 && y >= 2 && y < 9 - ((x - 4) / 3) % 2 {
|
||
Some(c(210, 40, 40))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
cv.cell(I_BOLT, |x, y| {
|
||
let line1 = y < 8 && (x - (10 - y / 2)).abs() <= 1;
|
||
let line2 = y >= 7 && y <= 9 && (5..=11).contains(&x);
|
||
let line3 = y > 8 && (x - (8 - (y - 9) / 2)).abs() <= 1;
|
||
if line1 || line2 || line3 {
|
||
Some(c(255, 240, 120))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
cv.cell(I_HEAL, |x, y| {
|
||
if ((6..=9).contains(&x) && (2..=13).contains(&y)) || ((2..=13).contains(&x) && (6..=9).contains(&y)) {
|
||
Some(c(90, 230, 110))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
cv.cell(I_ARROW, |x, y| {
|
||
// a small wooden tower with an arrow
|
||
if (4..=11).contains(&x) && y >= 7 {
|
||
Some(if (x + y) % 4 == 0 { c(90, 60, 34) } else { c(130, 88, 50) })
|
||
} else if y == 6 && x % 2 == 0 && (4..=11).contains(&x) {
|
||
Some(c(130, 88, 50))
|
||
} else if y == 3 && (2..=13).contains(&x) || (x == 12 && (y == 2 || y == 4)) {
|
||
Some(c(220, 220, 220))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
cv.cell(I_CANNON, |x, y| {
|
||
let barrel = y >= 4 && y <= 7 && x >= 5 && x <= 14 && (y - 4) as i32 <= 3;
|
||
let wheel = (x - 6) * (x - 6) + (y - 10) * (y - 10) <= 9;
|
||
if barrel {
|
||
Some(c(50, 50, 56))
|
||
} else if wheel {
|
||
Some(c(120, 80, 40))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
cv.cell(I_MAGIC, |x, y| {
|
||
let crystal = (x - 8).abs() + (y - 5).abs() <= 3;
|
||
if crystal {
|
||
Some(c(190, 110, 255))
|
||
} else if (6..=9).contains(&x) && y >= 9 {
|
||
Some(c(110, 110, 124))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
cv.cell(I_RUNE, |x, y| {
|
||
// a stone obelisk with a glowing rune, bronze bands
|
||
let body = (5..=10).contains(&x) && y >= 3 && y <= 14 && !(y == 3 && (x == 5 || x == 10));
|
||
if !body {
|
||
return None;
|
||
}
|
||
let rune = (x == 7 || x == 8) && (6..=10).contains(&y) || (y == 8 && (6..=9).contains(&x));
|
||
Some(if rune {
|
||
c(90, 210, 255)
|
||
} else if y == 4 || y == 12 {
|
||
c(190, 120, 60)
|
||
} else if (x + y) % 5 == 0 {
|
||
c(80, 80, 92)
|
||
} else {
|
||
c(110, 110, 124)
|
||
})
|
||
});
|
||
cv.cell(I_SUMMON, |x, y| {
|
||
let d = (((x - 8) * (x - 8) + (y - 8) * (y - 8)) as f32).sqrt();
|
||
let a = ((y - 8) as f32).atan2((x - 8) as f32);
|
||
if d < 7.0 && ((a * 3.0 + d * 0.9).sin() > 0.3) {
|
||
Some(c(200, 90, 230))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
// ---- campaign terrain
|
||
for v in 0..4u32 {
|
||
// water: dark blue with lighter wave crests. Seamless (period 16): the shader lets it flow
|
||
// across the tiles, sampled in world space
|
||
cv.cell(T_WATER + v, |x, y| {
|
||
let tau = std::f32::consts::TAU;
|
||
let bend = ((y as f32 + v as f32 * 3.0) * tau / 16.0).sin() * 1.6 + ((x as f32) * tau / 8.0).sin() * 0.6;
|
||
let wave = (x + y * 2 + bend.round() as i32).rem_euclid(8);
|
||
let d = (rnd(x, y, v + 120) * 10.0) as i32 - 5;
|
||
Some(match wave {
|
||
0 => shade((70, 120, 170), d),
|
||
1 => shade((50, 92, 140), d),
|
||
_ => shade((24, 50, 88), d),
|
||
})
|
||
});
|
||
// lava: molten orange with hot yellow streaks and darker red troughs, no black crust.
|
||
// Seamless (period 16) so it can flow across the tiles
|
||
cv.cell(T_LAVA + v, |x, y| {
|
||
let tau = std::f32::consts::TAU;
|
||
let (fx, fy) = (x as f32 / 16.0, y as f32 / 16.0);
|
||
let o = v as f32 * 1.3;
|
||
let heat = (tau * (fx + fy * 2.0) + o).sin() * 0.45
|
||
+ (tau * (fx * 3.0 - fy) + (tau * fy * 2.0 + o).sin() * 1.2).sin() * 0.35
|
||
+ (tau * (fy * 3.0 + fx * 2.0) + o * 2.0).sin() * 0.2;
|
||
let d = (rnd(x, y, v + 134) * 16.0) as i32 - 8;
|
||
Some(if heat > 0.62 {
|
||
shade((255, 228, 120), d / 2)
|
||
} else if heat > 0.3 {
|
||
shade((255, 170, 55), d)
|
||
} else if heat > -0.35 {
|
||
shade((240, 110, 25), d)
|
||
} else if heat > -0.7 {
|
||
shade((200, 64, 18), d)
|
||
} else {
|
||
shade((150, 40, 14), d)
|
||
})
|
||
});
|
||
// knight fortress walls: pale stone blocks with blue-grey mortar
|
||
cv.cell(T_FORTWALL + v, |x, y| {
|
||
let row = y / 5;
|
||
let off = if row % 2 == 1 { 5 } else { 0 };
|
||
if y % 5 == 4 || (x + off) % 10 == 9 {
|
||
return Some(shade((70, 76, 92), (rnd(x, y, v + 140) * 8.0) as i32));
|
||
}
|
||
let blk = hash((x + off) / 10, row, v + 141) % 4;
|
||
Some(shade((150, 150, 160), (rnd(x, y, v + 142) * 12.0) as i32 - 6 + blk as i32 * 5 - 8 + if y % 5 == 0 { 10 } else { 0 }))
|
||
});
|
||
}
|
||
for v in 0..2u32 {
|
||
// wooden bridge: planks across, two rope rails at the sides
|
||
cv.cell(T_BRIDGE + v, |x, y| {
|
||
if x <= 1 || x >= 14 {
|
||
return Some(if (y + v as i32) % 4 == 0 { c(150, 120, 80) } else { c(96, 70, 42) });
|
||
}
|
||
if y % 4 == 0 {
|
||
return Some(c(40, 26, 16));
|
||
}
|
||
let plank = y / 4;
|
||
let off = (hash(plank, 1, v + 150) % 16) as i32;
|
||
if (x + off) % 13 == 0 && y % 4 == 2 {
|
||
return Some(c(60, 40, 24)); // nail
|
||
}
|
||
Some(shade((126, 88, 52), (rnd(x, y, v + 151) * 14.0) as i32 - 7 + (plank % 2) * 6))
|
||
});
|
||
// stone bridge over lava: big grey slabs, sooty edges
|
||
cv.cell(T_STONEBRIDGE + v, |x, y| {
|
||
if x <= 1 || x >= 14 {
|
||
return Some(shade((60, 56, 56), (rnd(x, y, v + 160) * 10.0) as i32));
|
||
}
|
||
Some(stones(x, y, v + 161, (104, 98, 96), (46, 40, 40), 12))
|
||
});
|
||
}
|
||
// shard of the soul core: a glowing red crystal
|
||
cv.cell(D_SHARD, |x, y| {
|
||
let (dx, dy) = ((x - 8).abs(), y);
|
||
if y >= 2 && y <= 14 && dx * 3 <= (if y < 7 { (y - 2) * 4 } else { (15 - y) * 2 }) {
|
||
Some(if dx == 0 || (x + y) % 5 == 0 { c(255, 170, 170) } else if x < 8 { c(230, 50, 60) } else { c(160, 20, 40) })
|
||
} else if dy == 15 && (4..=12).contains(&x) {
|
||
Some(c(80, 30, 40))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
// knight banner: pole with a white-blue banner and a golden sun
|
||
cv.cell(D_BANNER, |x, y| {
|
||
if x == 3 {
|
||
return Some(c(170, 170, 180));
|
||
}
|
||
if (4..=13).contains(&x) && (1..=11).contains(&y) {
|
||
let (dx, dy) = (x - 9, y - 6);
|
||
if dx * dx + dy * dy <= 5 {
|
||
return Some(c(240, 200, 60));
|
||
}
|
||
return Some(if y == 11 && x % 2 == 0 { c(60, 90, 170) } else if x % 5 == 4 { c(200, 205, 220) } else { c(230, 232, 240) });
|
||
}
|
||
None
|
||
});
|
||
// bridge tool icon: an arch with planks
|
||
cv.cell(I_BRIDGE, |x, y| {
|
||
let (dx, dy) = (x as f32 - 7.5, y as f32 - 13.0);
|
||
let d = (dx * dx + dy * dy).sqrt();
|
||
if y <= 6 && y >= 5 && (1..=14).contains(&x) {
|
||
Some(if x % 3 == 0 { c(70, 46, 26) } else { c(150, 106, 60) })
|
||
} else if (6.0..7.5).contains(&d) && y >= 6 {
|
||
Some(c(120, 84, 48))
|
||
} else if y > 6 && (x == 1 || x == 14) {
|
||
Some(c(110, 76, 44))
|
||
} else if y >= 12 && d >= 7.5 && (2..=13).contains(&x) {
|
||
Some(c(60, 110, 170))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
// 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))
|
||
}
|
||
});
|
||
// 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<f32> {
|
||
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<Color> {
|
||
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<Color>; 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<Color>; 8] = [
|
||
// seer's eye
|
||
|x, y| {
|
||
let (dx, dy) = ((x - 8) as f32, (y - 8) as f32 * 1.8);
|
||
let d = (dx * dx + dy * dy).sqrt();
|
||
if d < 2.5 {
|
||
Some(c(40, 10, 60))
|
||
} else if d < 4.5 {
|
||
Some(c(200, 120, 255))
|
||
} else if d < 7.0 {
|
||
Some(c(235, 225, 245))
|
||
} else {
|
||
None
|
||
}
|
||
},
|
||
// cave-in
|
||
|x, y| {
|
||
if y >= 9 && (x + y * 3) % 5 != 0 {
|
||
Some(c(110, 80, 56))
|
||
} else if y < 9 && ((x * 7 + y * 5) % 9 == 0) {
|
||
Some(c(140, 110, 80))
|
||
} else if y < 3 {
|
||
Some(c(70, 60, 56))
|
||
} else {
|
||
None
|
||
}
|
||
},
|
||
// haste
|
||
|x, y| {
|
||
let bolt = (y < 8 && (x - (12 - y / 2)).abs() <= 1) || (y >= 8 && (x - (10 - (y - 8) / 2) + 4).abs() <= 1) || (y == 8 && (5..=11).contains(&x));
|
||
if bolt {
|
||
Some(c(255, 230, 90))
|
||
} else if (y == 4 || y == 11) && x < 4 {
|
||
Some(c(255, 200, 80))
|
||
} else {
|
||
None
|
||
}
|
||
},
|
||
// stoneskin
|
||
|x, y| {
|
||
if (3..=12).contains(&x) && (2..=13).contains(&y) && !(y > 10 && (x - 8).abs() + (y - 10) > 5) {
|
||
Some(if (x + y) % 4 == 0 { c(110, 110, 104) } else { c(170, 168, 160) })
|
||
} else {
|
||
None
|
||
}
|
||
},
|
||
// gold rain
|
||
|x, y| {
|
||
if y % 4 == (x / 3) % 4 && x % 3 != 2 {
|
||
Some(c(255, 210, 60))
|
||
} else {
|
||
None
|
||
}
|
||
},
|
||
// invisibility
|
||
|x, y| {
|
||
let d = (x - 8).pow(2) + (y - 7).pow(2);
|
||
if (d <= 20 && (x + y) % 2 == 0) || (y >= 11 && (x - 8).abs() <= 4 && (x + y) % 2 == 0) {
|
||
Some(c(200, 220, 255))
|
||
} else {
|
||
None
|
||
}
|
||
},
|
||
// plague
|
||
|x, y| {
|
||
let d = |cx: i32, cy: i32, r: i32| (x - cx).pow(2) + (y - cy).pow(2) <= r * r;
|
||
if d(8, 8, 5) && !d(8, 8, 2) {
|
||
Some(c(140, 210, 60))
|
||
} else if d(8, 8, 1) || d(3, 4, 1) || d(13, 12, 1) {
|
||
Some(c(90, 150, 40))
|
||
} else {
|
||
None
|
||
}
|
||
},
|
||
// quake
|
||
|x, y| {
|
||
if y >= 7 && y <= 9 && (x + (y - 8)) % 4 != 0 {
|
||
Some(c(140, 110, 80))
|
||
} else if (x - 8 + (y - 8).abs() * 0).abs() <= 0 && y > 9 {
|
||
Some(c(60, 40, 30))
|
||
} else if y > 9 && (x + y) % 3 == 0 {
|
||
Some(c(100, 76, 56))
|
||
} else if y < 6 && (x * 5 + y) % 7 == 0 {
|
||
Some(c(170, 140, 100))
|
||
} else {
|
||
None
|
||
}
|
||
},
|
||
];
|
||
for (k, f) in spells.iter().enumerate() {
|
||
cv.cell(I_SPELLS + k as u32, |x, y| f(x, y));
|
||
}
|
||
// order icons: crossed blades (attack), a shield (guard)
|
||
cv.cell(I_ORDERS, |x, y| {
|
||
let blade = (x - y).abs() <= 1 || (x + y - 15).abs() <= 1;
|
||
if blade && (2..=13).contains(&x) && (2..=13).contains(&y) {
|
||
Some(if (x - y).abs() == 0 || (x + y - 15) == 0 { c(240, 240, 250) } else { c(170, 170, 185) })
|
||
} else if (y == 12 || y == 13) && (x == 2 || x == 3 || x == 12 || x == 13) {
|
||
Some(c(140, 90, 40))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
cv.cell(I_ORDERS + 1, |x, y| {
|
||
let w = if y < 9 { 6 } else { 6 - (y - 8) };
|
||
if (2..=14).contains(&y) && (x - 8).abs() <= w.max(0) {
|
||
Some(if (x - 8).abs() == w.max(0) || y == 2 { c(220, 190, 80) } else if x == 8 || y == 6 { c(220, 190, 80) } else { c(150, 30, 36) })
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
// aerie: straw and twigs on stone, a few feathers
|
||
for v in 0..4u32 {
|
||
cv.cell(T_AERIE + v, |x, y| {
|
||
let base = stones(x, y, v + 230, (70, 62, 58), (36, 30, 30), 8);
|
||
let twig = (x * 3 + y * 7 + v as i32 * 5) % 13 == 0 || (x * 5 - y * 2 + v as i32) % 17 == 0;
|
||
let straw = rnd(x / 2, y, v + 231) > 0.62;
|
||
if hash(x, y, v + 232) % 41 == 0 {
|
||
Some(c(235, 232, 225)) // feather
|
||
} else if twig {
|
||
Some(c(96, 66, 40))
|
||
} else if straw {
|
||
Some(shade((190, 160, 80), (rnd(x, y, v + 233) * 20.0) as i32 - 10))
|
||
} else {
|
||
Some(base)
|
||
}
|
||
});
|
||
}
|
||
// floors of the rival masters: like the player's claimed floor, in their colours
|
||
for v in 0..4u32 {
|
||
for (id, base, grout, mark) in [(T_RIVAL1, (44, 74, 50), (20, 36, 24), (90, 170, 100)), (T_RIVAL2, (44, 54, 86), (20, 24, 44), (90, 120, 200)), (T_RIVAL3, (74, 62, 26), (36, 30, 12), (210, 185, 70))] {
|
||
cv.cell(id + v, |x, y| {
|
||
let col = stones(x, y, v + 250, base, grout, 10);
|
||
if (x % 8 == 4 && y % 8 == 4) && hash(x / 8, y / 8, v + 251) % 2 == 0 {
|
||
Some(c(mark.0, mark.1, mark.2))
|
||
} else {
|
||
Some(col)
|
||
}
|
||
});
|
||
}
|
||
}
|
||
// steel walls: riveted plates with a little rust
|
||
for v in 0..4u32 {
|
||
cv.cell(T_STEEL + v, |x, y| {
|
||
let seam = x % 8 == 0 || y % 8 == 0;
|
||
let rivet = (x % 8 == 2 || x % 8 == 6) && (y % 8 == 2 || y % 8 == 6);
|
||
let rust = rnd(x / 2, y / 2, v + 240) > 0.86;
|
||
if seam {
|
||
Some(c(44, 48, 58))
|
||
} else if rivet {
|
||
Some(c(170, 176, 190))
|
||
} else if rust {
|
||
Some(shade((120, 70, 40), (rnd(x, y, v + 241) * 16.0) as i32))
|
||
} else {
|
||
Some(shade((96, 102, 116), (rnd(x, y, v + 242) * 10.0) as i32 - 5 + if y % 8 == 1 { 10 } else { 0 }))
|
||
}
|
||
});
|
||
}
|
||
cv.cell(I_STEEL, |x, y| {
|
||
if (2..=13).contains(&x) && (3..=12).contains(&y) {
|
||
let rivet = (x == 4 || x == 11) && (y == 5 || y == 10);
|
||
Some(if rivet { c(220, 224, 235) } else if x == 2 || y == 3 || x == 13 || y == 12 { c(60, 64, 76) } else { c(130, 136, 150) })
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
// floor decals
|
||
cv.cell(D_RUBBLE, |x, y| {
|
||
for k in 0..9 {
|
||
let gx = 1 + (hash(k, 5, 220) % 14) as i32;
|
||
let gy = 1 + (hash(k, 6, 220) % 14) as i32;
|
||
let s = (hash(k, 7, 220) % 3) as i32;
|
||
if (x - gx).abs() + (y - gy).abs() <= s {
|
||
return Some(if y < gy { c(120, 110, 104) } else if (x + y) % 2 == 0 { c(84, 76, 72) } else { c(66, 60, 56) });
|
||
}
|
||
}
|
||
None
|
||
});
|
||
cv.cell(D_CRACK, |x, y| {
|
||
let cx = 8.0 + ((y as f32) * 0.7).sin() * 3.0 + (y as f32 * 1.9).sin();
|
||
if (x as f32 - cx).abs() < 0.7 || (y > 9 && (x as f32 - cx - (y - 9) as f32 * 0.8).abs() < 0.6) {
|
||
Some(Color::from_rgba(20, 14, 14, 200))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
cv.cell(D_RUBY, |x, y| gems(x, y, 200, c(255, 150, 170), c(215, 30, 60), c(120, 12, 36)));
|
||
cv.cell(D_DIAMOND, |x, y| gems(x, y, 210, c(255, 255, 255), c(170, 232, 255), c(80, 150, 200)));
|
||
cv.img
|
||
}
|
||
|
||
// ------------------------------------------------------------------ Sprites (10x10)
|
||
|
||
const SPR: usize = 10;
|
||
const SPR_CELL: u32 = 12;
|
||
|
||
fn sprite_rows(k: EK) -> [&'static str; 10] {
|
||
match k {
|
||
EK::Worker => [
|
||
"..........",
|
||
"k..kkkk..k",
|
||
"okkooookko",
|
||
".koeooeok.",
|
||
"..kooook..",
|
||
"...kRRk..S",
|
||
"..kRRRRkSS",
|
||
"..kRRRRkb.",
|
||
"...kRRkb..",
|
||
"...k..k...",
|
||
],
|
||
EK::Kobold => [
|
||
"........s.",
|
||
"..kkkk..s.",
|
||
".kggggk.b.",
|
||
".kgegegkb.",
|
||
".kggggkkb.",
|
||
"..kGGkggb.",
|
||
".kgGGGgkb.",
|
||
".kgGGGk.b.",
|
||
"..kGkGk...",
|
||
"..kk.kk...",
|
||
],
|
||
EK::Spider => [
|
||
"..........",
|
||
"...kkkk...",
|
||
"k.kPPPPk.k",
|
||
".kPPPPPPk.",
|
||
"kkPePPePkk",
|
||
".kPPPPPPk.",
|
||
"k.kPPPPk.k",
|
||
".k.kkkk.k.",
|
||
"k........k",
|
||
"..........",
|
||
],
|
||
EK::Mage => [
|
||
"....kk....",
|
||
"...kPPk...",
|
||
"..kPPPPk..",
|
||
".kkkkkkkk.",
|
||
"..kfefek.y",
|
||
"..kPPPPk.b",
|
||
".kPpPPpPkb",
|
||
".kPPpPPPkb",
|
||
".kPPPPPPk.",
|
||
"..kkkkkk..",
|
||
],
|
||
EK::Ogre => [
|
||
"..kkkkkk..",
|
||
".kcccccck.",
|
||
".kcecceck.",
|
||
".kcCccCck.",
|
||
"kcccccccck",
|
||
"kcCcccccCk",
|
||
"kccCccCcck",
|
||
".kcccccck.",
|
||
".kcCk.kCk.",
|
||
".kkk...kk.",
|
||
],
|
||
EK::Knight => [
|
||
"...kkkk...",
|
||
"..kssssk..",
|
||
"..kSkkSk.s",
|
||
"..kssssk.s",
|
||
"..kuuuuk.s",
|
||
".ksuuuusks",
|
||
".kSuyyuSk.",
|
||
"..kuuuuk..",
|
||
"..kSkkSk..",
|
||
"..kk..kk..",
|
||
],
|
||
EK::Archer => [
|
||
"...kkkk..b",
|
||
"..kGGGGkbk",
|
||
"..kGfkfkb.",
|
||
"..kGfffkb.",
|
||
"..kgggggb.",
|
||
".kfgggggb.",
|
||
"..kggggkbk",
|
||
"..kBBBBk.b",
|
||
"..kBkkBk..",
|
||
"..kk..kk..",
|
||
],
|
||
// 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<Color> {
|
||
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() * 12) as f32;
|
||
pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * SPR_CELL) as f32;
|
||
|
||
pub fn sprite_index(k: EK) -> u32 {
|
||
SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32
|
||
}
|
||
|
||
// Equipment tier of a level: 0 (Lv 1-2) none, 1 (3-4) shoulder pieces, 2 (5-6) + helmet,
|
||
// 3 (7-8) + cape, 4 (9-10) all in gold with a crown
|
||
pub const GEAR_TIERS: u32 = 5;
|
||
pub fn gear_tier(level: u32) -> u32 {
|
||
((level.max(1) - 1) / 2).min(GEAR_TIERS - 1)
|
||
}
|
||
|
||
fn is_hero_kind(k: EK) -> bool {
|
||
matches!(k, EK::Knight | EK::Archer | EK::Priest | EK::Dwarf | EK::Paladin | EK::Giant | EK::Fairy | EK::BattleMage | 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<usize> = (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<usize> = (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<Color>; 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<Color>; 12]; 12], y: usize| -> Option<(usize, usize)> {
|
||
let xs: Vec<usize> = (0..12).filter(|&x| g[y][x].is_some()).collect();
|
||
Some((*xs.first()?, *xs.last()?))
|
||
};
|
||
let top = (0..12).find(|&y| g[y].iter().any(|p| p.is_some())).unwrap_or(1);
|
||
// cape (behind the figure: only where the sprite is empty)
|
||
if tier >= 3 {
|
||
let cape = if is_hero_kind(k) { c(44, 74, 156) } else { c(128, 22, 34) };
|
||
for y in (top + 3).min(11)..=(top + 8).min(11) {
|
||
if let Some((l, r)) = span(&g, y) {
|
||
for x in [l.wrapping_sub(1), r + 1] {
|
||
if x < 12 && g[y][x].is_none() {
|
||
g[y][x] = Some(cape);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// shoulder pieces at the widest row of the upper body
|
||
let shoulder = (top + 2..(top + 6).min(11)).max_by_key(|&y| span(&g, y).map_or(0, |(l, r)| r - l)).unwrap_or(top + 3);
|
||
if let Some((l, r)) = span(&g, shoulder) {
|
||
for x in [l, r] {
|
||
g[shoulder][x] = Some(metal);
|
||
if shoulder + 1 < 12 && g[shoulder + 1][x].is_some() {
|
||
g[shoulder + 1][x] = Some(dark);
|
||
}
|
||
}
|
||
}
|
||
// helmet: the top row in metal, horns (creatures) or a plume (heroes)
|
||
if tier >= 2 {
|
||
if let Some((l, r)) = span(&g, top) {
|
||
for x in l..=r {
|
||
if g[top][x].is_some() {
|
||
g[top][x] = Some(metal);
|
||
}
|
||
}
|
||
if top >= 1 {
|
||
if is_hero_kind(k) {
|
||
g[top - 1][(l + r) / 2] = Some(c(200, 40, 40));
|
||
} else {
|
||
let bone = c(232, 222, 200);
|
||
if l >= 1 {
|
||
g[top - 1][l - 1] = Some(bone);
|
||
g[top][l - 1] = Some(bone);
|
||
}
|
||
if r + 1 < 12 {
|
||
g[top - 1][r + 1] = Some(bone);
|
||
g[top][r + 1] = Some(bone);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// crown
|
||
if tier >= 4 && top >= 1 {
|
||
if let Some((l, r)) = span(&g, top) {
|
||
let m = (l + r) / 2;
|
||
for x in [m.saturating_sub(1), m + 1] {
|
||
if x < 12 {
|
||
g[top - 1][x] = Some(c(255, 230, 90));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
g
|
||
}
|
||
|
||
// row (view * GEAR_TIERS + tier) * 2: sprites with the gear of the tier, the row after it:
|
||
// white silhouettes (hit flash)
|
||
fn gen_sprites() -> Image {
|
||
let n = SPRITE_KINDS.len() as u32;
|
||
let mut img = Image::gen_image_color((n * SPR_CELL) as u16, (2 * VIEWS * GEAR_TIERS * SPR_CELL) as u16, Color::new(0.0, 0.0, 0.0, 0.0));
|
||
for (i, &k) in SPRITE_KINDS.iter().enumerate() {
|
||
for view in 0..VIEWS {
|
||
for tier in 0..GEAR_TIERS {
|
||
let g = sprite_with_gear(k, tier, view);
|
||
for (y, row) in g.iter().enumerate() {
|
||
for (x, px) in row.iter().enumerate() {
|
||
if let Some(col) = px {
|
||
let (px, py) = (i as u32 * SPR_CELL + x as u32, (view * GEAR_TIERS + tier) * 2 * SPR_CELL + y as u32);
|
||
img.set_pixel(px, py, *col);
|
||
img.set_pixel(px, py + SPR_CELL, WHITE);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
img
|
||
}
|
||
|
||
// ------------------------------------------------------------------ mouse pointers (16x16)
|
||
|
||
pub const CURSOR_POINT: u32 = 0; // pointing monster hand (hot spot: finger tip)
|
||
pub const CURSOR_OPEN: u32 = 1; // open grabbing hand (hot spot: palm centre)
|
||
pub const CURSOR_FIST: u32 = 2; // holding something
|
||
pub const CURSOR_BACKHAND: u32 = 3; // slap, with motion streaks
|
||
pub const CURSOR_PUNCH: u32 = 4; // punch, with impact star
|
||
pub const CURSOR_HOT: [(f32, f32); 5] = [(2.0, 1.0), (7.0, 7.0), (7.0, 7.0), (7.0, 7.0), (7.0, 7.0)];
|
||
|
||
const CUR_POINT: [&str; 16] = [
|
||
"..kk............",
|
||
".kwwk...........",
|
||
".kggk...........",
|
||
".kggk...........",
|
||
".kggkkk.........",
|
||
".kggkggkkk......",
|
||
".kggkggkggkk....",
|
||
"kkggggggggggk...",
|
||
"kgkgggggggggk...",
|
||
"kggggggggggGk...",
|
||
".kgggggggggGk...",
|
||
"..kggggggggGk...",
|
||
"..kgggggggGk....",
|
||
"...kGGGGGGk.....",
|
||
"...kkkkkkkk.....",
|
||
"................",
|
||
];
|
||
// 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<Color> {
|
||
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);
|
||
Gfx { atlas, sprites, 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);
|
||
Rect::new((i * SPR_CELL + 1) as f32, (1 + if white { SPR_CELL } else { 0 }) as f32, SPR as f32, SPR as f32)
|
||
}
|
||
|
||
// the whole 12x12 cell with the gear of a level (billboards)
|
||
pub fn sprite_cell(k: EK, white: bool, level: u32) -> Rect {
|
||
Gfx::sprite_cell_view(k, white, level, 0)
|
||
}
|
||
|
||
// the cell of one of the five views (see VIEWS)
|
||
pub fn sprite_cell_view(k: EK, white: bool, level: u32, view: u32) -> Rect {
|
||
let i = sprite_index(k);
|
||
let row = (view.min(VIEWS - 1) * GEAR_TIERS + gear_tier(level)) * 2 + white as u32;
|
||
Rect::new((i * SPR_CELL) as f32, (row * SPR_CELL) as f32, SPR_CELL as f32, SPR_CELL as f32)
|
||
}
|
||
|
||
pub fn tile_tex(kind: Kind, owner: Owner, var: u8, reinforced: bool) -> u32 {
|
||
let claimed = owner == Owner::Player;
|
||
let hero = owner == Owner::Hero;
|
||
let v = var as u32 & 3;
|
||
match kind {
|
||
Kind::Rock => T_ROCK + v,
|
||
Kind::Earth if reinforced && hero => T_FORTWALL + v,
|
||
Kind::Earth if reinforced => T_REINF + v,
|
||
Kind::Earth => T_EARTH + v,
|
||
Kind::Gold => T_GOLD + v,
|
||
Kind::Silver => T_SILVER + (v & 1),
|
||
Kind::Ruby => T_RUBY + (v & 1),
|
||
Kind::Diamond => T_DIAMOND + (v & 1),
|
||
Kind::Floor => {
|
||
if claimed {
|
||
T_CLAIM + v
|
||
} else if owner == Owner::Rival1 {
|
||
T_RIVAL1 + v
|
||
} else if owner == Owner::Rival2 {
|
||
T_RIVAL2 + v
|
||
} else if owner == Owner::Rival3 {
|
||
T_RIVAL3 + v
|
||
} else if hero {
|
||
T_HEROFLOOR + v
|
||
} else {
|
||
T_FLOOR + v
|
||
}
|
||
}
|
||
Kind::Treasury => T_TREASURY + v,
|
||
Kind::Lair => T_LAIR + v,
|
||
Kind::Farm => T_FARM + v,
|
||
Kind::Training => T_TRAINING + v,
|
||
Kind::Library => T_LIBRARY + v,
|
||
Kind::Core => T_COREFLOOR + v,
|
||
Kind::Portal => T_PORTALFLOOR + v,
|
||
Kind::Gate => T_GATEFLOOR + v,
|
||
Kind::Torture => T_TORTURE + v,
|
||
Kind::Temple => T_TEMPLE + v,
|
||
Kind::Tower => T_CLAIM + v,
|
||
Kind::Water => T_WATER + v,
|
||
Kind::Lava => T_LAVA + v,
|
||
Kind::Bridge | 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
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn draw_cell(&self, id: u32, x: f32, y: f32, w: f32, h: f32, color: Color) {
|
||
draw_texture_ex(
|
||
&self.atlas,
|
||
x,
|
||
y,
|
||
color,
|
||
DrawTextureParams { dest_size: Some(vec2(w, h)), source: Some(Self::cell(id)), ..Default::default() },
|
||
);
|
||
}
|
||
|
||
// draw a single character (x, y = top-left corner)
|
||
pub fn glyph(&self, ch: char, x: f32, y: f32, scale: f32, color: Color) {
|
||
let (sx, sy) = font_src(&self.font_map, ch);
|
||
let src = Rect::new(sx, sy, GW, GH);
|
||
draw_texture_ex(
|
||
&self.font,
|
||
x.floor(),
|
||
y.floor(),
|
||
color,
|
||
DrawTextureParams { dest_size: Some(vec2(GW * scale, GH * scale)), source: Some(src), ..Default::default() },
|
||
);
|
||
}
|
||
|
||
// draw text (x, y = top-left corner); scale should be an integer
|
||
pub fn text(&self, s: &str, x: f32, y: f32, scale: f32, color: Color) {
|
||
let mut cx = x;
|
||
for ch in s.chars() {
|
||
if ch != ' ' {
|
||
self.glyph(ch, cx, y, scale, color);
|
||
}
|
||
cx += GW * scale;
|
||
}
|
||
}
|
||
|
||
pub fn measure(&self, s: &str, scale: f32) -> f32 {
|
||
s.chars().count() as f32 * GW * scale
|
||
}
|
||
|
||
// Military rank badge for a level (k = size of one badge pixel in device pixels):
|
||
// Lv 2-4: 1-3 bronze chevrons, Lv 5-7: 1-3 silver bars, Lv 8-9: 1-2 gold stars,
|
||
// Lv 10: a gold star in a green wreath. Lv 1 has none. Returns the width drawn.
|
||
pub fn badge(&self, x: f32, y: f32, level: u32, k: f32) -> f32 {
|
||
let px = |gx: i32, gy: i32, c: Color| draw_rectangle(x + gx as f32 * k, y + gy as f32 * k, k, k, c);
|
||
let plate = |w: i32, h: i32| draw_rectangle(x - k, y - k, (w + 2) as f32 * k, (h + 2) as f32 * k, Color::new(0.05, 0.03, 0.04, 0.85));
|
||
let bronze = Color::from_rgba(214, 140, 70, 255);
|
||
let silver = Color::from_rgba(215, 222, 235, 255);
|
||
let gold = Color::from_rgba(255, 210, 70, 255);
|
||
let star = |ox: i32, oy: i32, c: Color| {
|
||
for (gx, gy) in [(2, 0), (1, 1), (2, 1), (3, 1), (0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (1, 3), (2, 3), (3, 3), (0, 4), (1, 4), (3, 4), (4, 4)] {
|
||
px(ox + gx, oy + gy, c);
|
||
}
|
||
};
|
||
match level {
|
||
0 | 1 => 0.0,
|
||
2..=4 => {
|
||
let n = (level - 1) as i32;
|
||
plate(5, n * 2 + 1);
|
||
for c in 0..n {
|
||
// a chevron pointing up
|
||
for (gx, gy) in [(2, 0), (1, 1), (3, 1), (0, 2), (4, 2)] {
|
||
px(gx, c * 2 + gy - if gy == 2 { 1 } else { 0 }, bronze);
|
||
}
|
||
}
|
||
7.0 * k
|
||
}
|
||
5..=7 => {
|
||
let n = (level - 4) as i32;
|
||
plate(5, n * 2 - 1);
|
||
for b in 0..n {
|
||
for gx in 0..5 {
|
||
px(gx, b * 2, silver);
|
||
}
|
||
}
|
||
7.0 * k
|
||
}
|
||
8 | 9 => {
|
||
let n = (level - 7) as i32;
|
||
plate(n * 6 - 1, 5);
|
||
for s in 0..n {
|
||
star(s * 6, 0, gold);
|
||
}
|
||
(n * 6 + 1) as f32 * k
|
||
}
|
||
_ => {
|
||
plate(9, 7);
|
||
let green = Color::from_rgba(90, 170, 80, 255);
|
||
for (gx, gy) in [(0, 2), (0, 3), (0, 4), (1, 5), (2, 6), (8, 2), (8, 3), (8, 4), (7, 5), (6, 6), (3, 6), (5, 6), (1, 1), (7, 1)] {
|
||
px(gx, gy, green);
|
||
}
|
||
star(2, 1, gold);
|
||
11.0 * k
|
||
}
|
||
}
|
||
}
|
||
|
||
// text with a dark shadow
|
||
pub fn text_sh(&self, s: &str, x: f32, y: f32, scale: f32, color: Color) {
|
||
self.text(s, x + scale, y + scale, scale, Color::new(0.0, 0.0, 0.0, color.a * 0.85));
|
||
self.text(s, x, y, scale, color);
|
||
}
|
||
|
||
// window with a box-drawing frame; size in characters (cols x rows incl. frame)
|
||
pub fn window(&self, x: f32, y: f32, cols: i32, rows: i32, scale: f32, double: bool, frame: Color, bg: Color) {
|
||
let cw = GW * scale;
|
||
let ch = GH * scale;
|
||
draw_rectangle(x, y, cols as f32 * cw, rows as f32 * ch, bg);
|
||
let b = if double { BOX_DOUBLE } else { BOX_SINGLE };
|
||
for cx in 0..cols {
|
||
for cy in 0..rows {
|
||
let top = cy == 0;
|
||
let bottom = cy == rows - 1;
|
||
let left = cx == 0;
|
||
let right = cx == cols - 1;
|
||
let code = match (top, bottom, left, right) {
|
||
(true, _, true, _) => b[0],
|
||
(true, _, _, true) => b[1],
|
||
(_, true, true, _) => b[2],
|
||
(_, true, _, true) => b[3],
|
||
(true, _, _, _) | (_, true, _, _) => b[4],
|
||
(_, _, true, _) | (_, _, _, true) => b[5],
|
||
_ => continue,
|
||
};
|
||
self.glyph(code, x + cx as f32 * cw, y + cy as f32 * ch, scale, frame);
|
||
}
|
||
}
|
||
}
|
||
|
||
// window with a title in the top frame line
|
||
pub fn window_titled(&self, x: f32, y: f32, cols: i32, rows: i32, scale: f32, double: bool, frame: Color, bg: Color, title: &str, tcol: Color) {
|
||
self.window(x, y, cols, rows, scale, double, frame, bg);
|
||
if !title.is_empty() {
|
||
let cw = GW * scale;
|
||
let t = format!(" {} ", title);
|
||
let n = t.chars().count() as i32;
|
||
// the frame line ends cleanly on both sides of the title (rope ends, or the double
|
||
// strand's caps), if there is room for them
|
||
let caps = n + 5 <= cols;
|
||
let tx = x + if caps { 1.0 } else { 2.0 } * cw;
|
||
let w = cw * (n + if caps { 2 } else { 0 }) as f32;
|
||
draw_rectangle(tx, y, w, GH * scale, bg);
|
||
if caps {
|
||
let (l, r) = if double { ('╡', '╞') } else { ('╴', '╶') };
|
||
self.glyph(l, tx, y, scale, frame);
|
||
self.text(&t, tx + cw, y, scale, tcol);
|
||
self.glyph(r, tx + cw * (n + 1) as f32, y, scale, frame);
|
||
} else {
|
||
self.text(&t, tx, y, scale, tcol);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod doc_images {
|
||
use super::*;
|
||
|
||
// writes a picture scaled up with nearest pixels
|
||
fn save(img: &Image, src: Rect, scale: u32, path: &str) {
|
||
let (w, h) = (src.w as u32 * scale, src.h as u32 * scale);
|
||
let mut out = Image::gen_image_color(w as u16, h as u16, Color::new(0.0, 0.0, 0.0, 0.0));
|
||
for y in 0..h {
|
||
for x in 0..w {
|
||
let p = img.get_pixel(src.x as u32 + x / scale, src.y as u32 + y / scale);
|
||
// export_png writes the rows bottom up: store them flipped
|
||
out.set_pixel(x, h - 1 - y, p);
|
||
}
|
||
}
|
||
out.export_png(path);
|
||
}
|
||
|
||
// `cargo test --release export_doc_images -- --ignored` refreshes the pictures in docs/img
|
||
#[test]
|
||
#[ignore]
|
||
fn export_doc_images() {
|
||
std::fs::create_dir_all("docs/img/units").unwrap();
|
||
std::fs::create_dir_all("docs/img/icons").unwrap();
|
||
let sprites = gen_sprites();
|
||
for &k in SPRITE_KINDS.iter() {
|
||
let id = crate::game::st(k).name.trim_start_matches("unit.");
|
||
for (lv, tag) in [(1u32, "1"), (5, "5"), (10, "10")] {
|
||
save(&sprites, Gfx::sprite_cell(k, false, lv), 8, &format!("docs/img/units/{}_{}.png", id, tag));
|
||
}
|
||
}
|
||
let atlas = gen_atlas();
|
||
for (k, t) in crate::traps::TRAP_KINDS.iter().enumerate() {
|
||
save(&atlas, Gfx::cell(I_TRAPS + k as u32), 4, &format!("docs/img/icons/{}.png", t.id()));
|
||
}
|
||
for 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<String> {
|
||
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<String> = 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);
|
||
}
|
||
}
|
||
}
|