Files
dungeoncraft/src/gfx.rs
T

2451 lines
89 KiB
Rust
Raw Blame History

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