2951 lines
111 KiB
Rust
2951 lines
111 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;
|
||
pub 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 hd_sprites: Texture2D, // detailed sprites (see HD_SPRITES, hd_cell_view)
|
||
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 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)
|
||
}
|
||
|
||
// ---- 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;
|
||
// (first cell, variants, 64x64 level, 128x128 level)
|
||
const HD_TILES: [(u32, u32, &[u8], &[u8]); 10] = [
|
||
(T_FLOOR, 4, include_bytes!("../assets/textures/floor.png"), include_bytes!("../assets/textures/floor-256.png")),
|
||
(T_CLAIM, 4, include_bytes!("../assets/textures/claimed.png"), include_bytes!("../assets/textures/claimed-256.png")),
|
||
(T_EARTH, 4, include_bytes!("../assets/textures/earth.png"), include_bytes!("../assets/textures/earth-256.png")),
|
||
(T_ROCK, 4, include_bytes!("../assets/textures/rock.png"), include_bytes!("../assets/textures/rock-256.png")),
|
||
(T_GOLD, 4, include_bytes!("../assets/textures/gold.png"), include_bytes!("../assets/textures/gold-256.png")),
|
||
(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")),
|
||
(T_SILVER, 2, include_bytes!("../assets/textures/silver.png"), include_bytes!("../assets/textures/silver-128.png")),
|
||
(T_RUBY, 2, include_bytes!("../assets/textures/ruby.png"), include_bytes!("../assets/textures/ruby-128.png")),
|
||
(T_DIAMOND, 2, include_bytes!("../assets/textures/diamond.png"), include_bytes!("../assets/textures/diamond-128.png")),
|
||
];
|
||
|
||
// the cell a tile at (x, z) shows in the high-res style: generated textures with four variants
|
||
// are laid out by the position (see HD_TILES), everything else stays
|
||
pub fn hd_tile(id: u32, x: i32, z: i32) -> u32 {
|
||
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,
|
||
}
|
||
}
|
||
|
||
// 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));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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 {
|
||
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;
|
||
// 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/) in their own texture, used instead of the
|
||
// 10x10 ones in the high-res graphics style (render3d::billboards). Two levels of detail from
|
||
// the same pictures: 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); `hack*`: the digging animation, HACK_FRAMES
|
||
// frames side by side, one row per view. No gear yet.
|
||
struct HdSprite {
|
||
kind: EK,
|
||
idle: &'static [u8],
|
||
hack: &'static [u8],
|
||
idle_32: &'static [u8],
|
||
hack_32: &'static [u8],
|
||
}
|
||
const HD_SPRITES: [HdSprite; 1] = [HdSprite {
|
||
kind: EK::Worker,
|
||
idle: include_bytes!("../assets/sprites/imp-64.png"),
|
||
hack: include_bytes!("../assets/sprites/imp-hack-64.png"),
|
||
idle_32: include_bytes!("../assets/sprites/imp-32.png"),
|
||
hack_32: include_bytes!("../assets/sprites/imp-hack-32.png"),
|
||
}];
|
||
pub const HACK_FRAMES: u32 = 4;
|
||
// 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
|
||
pub const HD_CELL: u32 = SPR_CELL * 6;
|
||
// columns per detailed sprite and detail level: idle and the hack frames, each without and with
|
||
// the sack; then the same for the 32x32 level
|
||
const HD_COLS: u32 = (1 + HACK_FRAMES) * 2;
|
||
pub const HD_TEX_W: f32 = (HD_SPRITES.len() as u32 * HD_COLS * 2 * HD_CELL) as f32;
|
||
pub const HD_TEX_H: f32 = (VIEWS * 2 * HD_CELL) as f32;
|
||
|
||
// 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<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() + 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<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
|
||
}
|
||
|
||
type Cell = Vec<Vec<Option<Color>>>;
|
||
|
||
// 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<usize> = (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<usize> = 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
|
||
}
|
||
|
||
// The texture of the detailed sprites (HD_SPRITES): per sprite HD_COLS columns of 64x64
|
||
// pictures, then HD_COLS of 32x32 ones (two texels per pixel); row view * 2, the row after it
|
||
// the white silhouette.
|
||
fn gen_hd_sprites() -> Image {
|
||
let mut img = Image::gen_image_color(HD_TEX_W as u16, HD_TEX_H as u16, Color::new(0.0, 0.0, 0.0, 0.0));
|
||
let png = |b: &[u8]| Image::from_file_with_format(b, Some(ImageFormat::Png)).expect("hd sprite");
|
||
for (j, hd) in HD_SPRITES.iter().enumerate() {
|
||
// (idle, hack, picture size, cell grid, texels per pixel, sack, where the sack shows from the front)
|
||
let levels = [
|
||
(png(hd.idle), png(hd.hack), 64, HD_CELL, 1, &SACK_64[..], (0.7, -0.65)),
|
||
(png(hd.idle_32), png(hd.hack_32), 32, HD_CELL / 2, 2, &SACK_32[..], (0.7, -0.65)),
|
||
];
|
||
for (l, (idle, hack, n, size, k, sack, dx)) in levels.iter().enumerate() {
|
||
for view in 0..VIEWS {
|
||
for f in 0..1 + HACK_FRAMES {
|
||
let g = if f == 0 { hd_cell(idle, view * n, 0, *n, *size) } else { hd_cell(hack, (f - 1) * n, view * n, *n, *size) };
|
||
let col = (j as u32 * 2 + l as u32) * HD_COLS + f * 2;
|
||
put_cell(&mut img, col, view * 2, &g, *k, HD_CELL);
|
||
let mut s = g;
|
||
add_sack(&mut s, view, sack, *dx);
|
||
put_cell(&mut img, col + 1, view * 2, &s, *k, HD_CELL);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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);
|
||
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 hd_sprites = Texture2D::from_image(&gen_hd_sprites());
|
||
hd_sprites.set_filter(FilterMode::Nearest);
|
||
Gfx { atlas, sprites, hd_sprites, atlas_hd, atlas_hd2, 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 cell of a detailed sprite in `hd_sprites`, if the kind has one: a view, standing
|
||
// (`hack` None) or a frame of the digging animation, with or without the sack; `fine`: the
|
||
// 64x64 level, else the 32x32 one
|
||
pub fn hd_cell_view(k: EK, white: bool, view: u32, hack: Option<u32>, sack: bool, fine: bool) -> Option<Rect> {
|
||
let j = HD_SPRITES.iter().position(|h| h.kind == k)? as u32;
|
||
let f = hack.map_or(0, |f| 1 + f % HACK_FRAMES);
|
||
let col = (j * 2 + !fine as u32) * HD_COLS + f * 2 + sack as u32;
|
||
let row = view.min(VIEWS - 1) * 2 + white as u32;
|
||
Some(Rect::new((col * HD_CELL) as f32, (row * HD_CELL) as f32, HD_CELL as f32, HD_CELL as f32))
|
||
}
|
||
|
||
// 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<Rect> {
|
||
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
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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 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)
|
||
};
|
||
// a cell without an own texture: the 16x16 one scaled up
|
||
let (cx, cy) = ((T_TREASURY % ATLAS_COLS) * 16, (T_TREASURY / 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) };
|
||
assert_eq!(px(&hd, ox + 7, oy + 40), px(&src, sx + 7, sy + 40));
|
||
}
|
||
}
|
||
}
|
||
// four variants by position, two variants keep theirs, others stay
|
||
assert_eq!(hd_tile(T_FLOOR + 3, 0, 0), T_FLOOR);
|
||
assert_eq!(hd_tile(T_FLOOR, 1, 1), T_FLOOR + 3);
|
||
assert_eq!(hd_tile(T_RUBY + 1, 1, 0), T_RUBY + 1);
|
||
assert_eq!(hd_tile(T_TREASURY + 2, 1, 0), T_TREASURY + 2);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod sprite_tests {
|
||
use super::*;
|
||
|
||
fn opaque(img: &Image, r: Rect) -> usize {
|
||
let n = r.w as u32;
|
||
(0..n * n).filter(|i| img.get_pixel(r.x as u32 + i % n, r.y as u32 + i / n).a > 0.5).count()
|
||
}
|
||
|
||
// 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()
|
||
}
|
||
|
||
#[test]
|
||
fn detailed_sprites() {
|
||
let img = gen_hd_sprites();
|
||
assert_eq!((img.width as f32, img.height as f32), (HD_TEX_W, HD_TEX_H));
|
||
for fine in [true, false] {
|
||
for view in 0..VIEWS {
|
||
for hack in [None, Some(0), Some(1), Some(2), Some(3)] {
|
||
let r = Gfx::hd_cell_view(EK::Worker, false, view, hack, false, fine).expect("the imp has a detailed sprite");
|
||
let w = Gfx::hd_cell_view(EK::Worker, true, view, hack, false, fine).unwrap();
|
||
let s = Gfx::hd_cell_view(EK::Worker, false, view, hack, true, fine).unwrap();
|
||
assert!(opaque(&img, r) > 600, "view {view} {hack:?} {fine} drawn");
|
||
assert_eq!(opaque(&img, r), opaque(&img, w), "white silhouette matches");
|
||
assert!(differ(&img, s, r) > 60, "view {view} {hack:?} {fine}: the sack shows");
|
||
}
|
||
}
|
||
}
|
||
assert!(Gfx::hd_cell_view(EK::Knight, false, 0, None, false, true).is_none());
|
||
}
|
||
|
||
#[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 cells to /tmp for
|
||
// a look: rows idle and the hack frames, without and with sack; 64x64 left, 32x32 right
|
||
#[test]
|
||
#[ignore]
|
||
fn dump_imp_cells() {
|
||
let img = gen_hd_sprites();
|
||
let mut rows: Vec<Vec<Rect>> = Vec::new();
|
||
for sack in [false, true] {
|
||
for hack in [None, Some(0), Some(1), Some(2), Some(3)] {
|
||
let mut r: Vec<Rect> = (0..VIEWS).map(|v| Gfx::hd_cell_view(EK::Worker, false, v, hack, sack, true).unwrap()).collect();
|
||
r.extend((0..VIEWS).map(|v| Gfx::hd_cell_view(EK::Worker, false, v, hack, sack, false).unwrap()));
|
||
rows.push(r);
|
||
}
|
||
}
|
||
let c = HD_CELL;
|
||
let (w, h) = (c * VIEWS * 2, 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, r) in list.iter().enumerate() {
|
||
for y in 0..c {
|
||
for x in 0..c {
|
||
let p = img.get_pixel(r.x as u32 + x, r.y as u32 + 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");
|
||
}
|
||
}
|
||
|
||
#[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<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);
|
||
}
|
||
}
|
||
}
|