Files
dungeoncraft/src/gfx.rs
T

3631 lines
143 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;
pub const ATLAS_COLS: u32 = 16;
pub const ATLAS_ROWS: u32 = 18; // rows 14-17: cells only the high-res atlases fill (see HD_SETS)
// atlas indices (4 variants each)
pub const T_ROCK: u32 = 0;
pub const T_EARTH: u32 = 4;
pub const T_GOLD: u32 = 8;
pub const T_FLOOR: u32 = 12;
pub const T_CLAIM: u32 = 16;
pub const T_HEROFLOOR: u32 = 20;
pub const T_TREASURY: u32 = 24;
pub const T_LAIR: u32 = 28;
pub const T_FARM: u32 = 32;
pub const T_TRAINING: u32 = 36;
pub const T_LIBRARY: u32 = 40;
pub const T_COREFLOOR: u32 = 44;
pub const T_PORTALFLOOR: u32 = 48;
pub const T_GATEFLOOR: u32 = 52;
// single cells
pub const T_FACE_EARTH: u32 = 56;
pub const T_FACE_ROCK: u32 = 57;
pub const T_FACE_GOLD: u32 = 58;
pub const T_MARK: u32 = 59;
pub const D_BED: u32 = 60;
pub const D_MUSHROOM: u32 = 61;
pub const D_DUMMY: u32 = 62;
pub const D_COINS: u32 = 63;
pub const D_SHELF: u32 = 64;
pub const I_PICK: u32 = 65;
pub const I_HAND: u32 = 66;
pub const I_SELL: u32 = 67;
pub const I_FLAG: u32 = 68;
pub const I_BOLT: u32 = 69;
pub const I_HEAL: u32 = 70;
pub const I_SUMMON: u32 = 71;
pub const T_REINF: u32 = 72; // 4 variants: reinforced wall masonry
pub const T_TORTURE: u32 = 76; // 4 variants: torture chamber floor
pub const D_RACK: u32 = 80;
pub const I_ARROW: u32 = 81;
pub const I_CANNON: u32 = 82;
pub const I_MAGIC: u32 = 83;
pub const T_TEMPLE: u32 = 84; // 4 variants
pub const D_ALTAR: u32 = 88;
pub const T_WATER: u32 = 96; // 4 variants
pub const T_LAVA: u32 = 100; // 4 variants
pub const T_BRIDGE: u32 = 104; // 2 variants: wooden planks
pub const T_STONEBRIDGE: u32 = 106; // 2 variants: stone slabs (over lava)
pub const T_FORTWALL: u32 = 108; // 4 variants: knight fortress masonry
pub const D_SHARD: u32 = 112; // shard of the soul core
pub const D_BANNER: u32 = 113; // knight banner
pub const I_BRIDGE: u32 = 114;
pub const T_SILVER: u32 = 115; // 2 variants each: veins in the earth
pub const T_RUBY: u32 = 117;
pub const T_DIAMOND: u32 = 119;
pub const D_SILVER: u32 = 121; // treasury content
pub const D_RUBY: u32 = 122;
pub const D_DIAMOND: u32 = 123;
pub const D_RUBBLE: u32 = 124; // floor decals: pebbles and fragments
pub const D_CRACK: u32 = 125; // a long crack through several stones
pub const T_AERIE: u32 = 126; // 4 variants: straw, twigs and feathers
pub const T_STEEL: u32 = 130; // 4 variants: riveted iron plates
pub const I_STEEL: u32 = 134;
pub const T_RIVAL1: u32 = 135; // 4 variants: floor of the green master
pub const T_RIVAL2: u32 = 139; // 4 variants: floor of the blue master
pub const I_RUNE: u32 = 143; // rune forge tower
pub const T_FORGE: u32 = 144; // 4 variants: sooty flagstones with glowing cracks
pub const I_TRAPS: u32 = 148; // 10 icons in TRAP_KINDS order
pub const I_SPELLS: u32 = 158; // 8 icons in SPELLS order
pub const T_PRISON: u32 = 166; // 4 variants each
pub const T_GRAVEYARD: u32 = 170;
pub const T_GUARDPOST: u32 = 174;
pub const T_ARENA: u32 = 178;
pub const T_ALTAR: u32 = 182;
pub const I_ORDERS: u32 = 186; // attack, guard
pub const T_RIVAL3: u32 = 189; // 4 variants: floor of the yellow master
pub const I_DRAWBRIDGE: u32 = 193; // drawbridge tool: a raised flap on chains
pub const I_FREEZE: u32 = 194; // frost spell: a snowflake
pub const T_OBSIDIAN: u32 = 195; // 4 variants: cooled lava, dark glass with glowing seams
pub const T_STEM: u32 = 199; // giant mushroom stem
pub const T_CAP: u32 = 200; // giant mushroom cap, glowing spots
pub const T_BARK: u32 = 201; // tree trunk
pub const T_LEAVES: u32 = 202; // 2 variants: tree crown
pub const T_GRASS: u32 = 204; // 4 variants: grass above ground
pub const I_WALL: u32 = 208; // wall tool: bricks rising from the floor
pub const I_RAM: u32 = 209; // battering ram tool
pub const I_CATAPULT: u32 = 210; // catapult tool
pub const T_SNOW: u32 = 211; // 4 variants: snow on the land above ground (winter)
pub const T_PUMPKIN: u32 = 215; // a carved pumpkin (Halloween)
pub const T_MOSS: u32 = 216; // 4 variants: mushroom grove ground, moss with glowing spores
pub const T_GIFT: u32 = 220; // a wrapped present (Christmas)
pub const T_FIR: u32 = 221; // fir needles with baubles (Christmas)
pub const T_EGG: u32 = 222; // 2 variants: a painted egg (Easter)
pub struct Gfx {
pub atlas: Texture2D,
pub sprites: Texture2D,
pub hd_sprites: Texture2D, // detailed sprites, 64x64 level (see HD_SPRITES, hd_picture)
pub hd_sprites32: Texture2D, // the same, 32x32 level
pub gear: Texture2D, // their gear parts (see GEAR_HELMET etc.)
pub atlas_hd: Texture2D, // the tile atlas of the high-res style, 64x64 tiles (see HD_TILES)
pub atlas_hd2: Texture2D, // the same with 128x128 tiles (close up)
pub materials: [Texture2D; 8], // furniture materials, high-res (see crate::props::Mat)
pub materials_px: [Texture2D; 8], // the same for the pixel-art style (16x16)
pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*)
pub badges: Texture2D, // achievement badges, 32x32 each (see badges.rs)
pub font: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE
pub font_map: FontMap, // which glyphs the two fonts hold
}
// ------------------------------------------------------------------ bitmap font (Dinowyde)
//
// assets/dinowyde_1x.png: 6x8 px glyphs, ordered by Unicode in five columns of 16 glyphs
// (96 px) and 48 rows each:
// column 0: U+0000-02FF (Basic Latin, Latin-1, Latin Extended A/B, IPA, spacing modifiers)
// column 1: U+0310-05FF (combining marks from U+0310, Greek, Cyrillic, Armenian, Hebrew;
// U+0300-030F have no slot)
// column 2: U+0600-06FF (Arabic) in rows 0-15, U+2000-21FF (punctuation ... arrows) in rows 16-47
// column 3: U+2200-24FF (mathematical operators, technical, ...)
// column 4: U+2500-27FF (box drawing, blocks, geometric shapes, ...)
pub const GW: f32 = 6.0; // glyph width
pub const GH: f32 = 8.0; // glyph height
const FONT_COLS: usize = 80; // glyphs per row of the sheet
const FONT_ROWS: usize = 48;
// place of a character in the sheet (glyph index row * FONT_COLS + column), if the sheet has a slot for it
pub fn font_slot(ch: char) -> Option<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)
}
thread_local! {
// the interface draws atlas cells from the high-res atlas (the high-res graphics style)
static HIGH_RES_UI: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub fn set_high_res_ui(on: bool) {
HIGH_RES_UI.with(|h| h.set(on));
}
pub fn high_res_ui() -> bool {
HIGH_RES_UI.with(|h| h.get())
}
// ---- furniture materials (crate::props::Mat order): grey detail the furniture's colours tint
// (the page keeps its colours), 64x64 and 16x16; see art/generated/textures/materials/
macro_rules! material {
($name:literal) => {
(include_bytes!(concat!("../assets/textures/mat/", $name, "-64.png")) as &[u8], include_bytes!(concat!("../assets/textures/mat/", $name, "-16.png")) as &[u8])
};
}
const MATERIALS: [(&[u8], &[u8]); 8] = [
material!("wood"),
material!("stone"),
material!("iron"),
material!("cloth"),
material!("straw"),
material!("fur"),
material!("book"),
material!("paper"),
];
// ---- high-res tiles
// The high-res style's atlases: the same cells as the 16x16 atlas, HD_TILE (64) or HD_TILE2 (128)
// texels each; the world shows the 128 one close up, where a tile gets that many pixels (two
// levels like the sprites: neither loses pixels nor flickers). Cells without an own texture are
// the 16x16 ones scaled up. HD_TILES: generated textures (see art/generated/textures/): for kinds
// with four variants a seamless 2x2 block of tiles (variant k = quarter k % 2, k / 2; the world
// lays the quarters out by the tiles' positions, see hd_tile, so a floor repeats only every two
// tiles and has no seams), for kinds with two variants one seamless tile for both.
pub const HD_TILE: u32 = 64;
pub const HD_TILE2: u32 = 128;
// the liquids: (first cell, variants, 64x64 level, 128x128 level); a seamless 2x2 block of
// tiles each (variant k = quarter k % 2, k / 2), which the shader lays out by position while
// the texture flows
const HD_TILES: [(u32, u32, &[u8], &[u8]); 2] = [
(T_WATER, 4, include_bytes!("../assets/textures/water.png"), include_bytes!("../assets/textures/water-256.png")),
(T_LAVA, 4, include_bytes!("../assets/textures/lava.png"), include_bytes!("../assets/textures/lava-256.png")),
];
// Sets of five tiles that fit together in any order, so a floor or a wall shows no pattern:
// the ground kinds (all five tiles share one seamless border, their middles differ; see
// tools/make_tile.py --variants), the masters' claimed floors as well: fitted flagstones of
// different sizes, some cracked, chipped or broken, in the master's colour with faint carved
// symbols of their own (the player crimson with demonic runes, Mossgrim mossy green with
// mushrooms and mould, Lady Nightwell midnight blue with lilies and moons, Saffrax ochre with
// alchemical sigils). They go
// into cells of their own from T_HDSET on, five per set; a tile picks one by its position,
// weighted. (the kind's cells: first and count, the first own cell, 64x64 tiles side by side,
// 128x128 ones, weights)
pub const T_HDSET: u32 = 224;
// the decals (wear and tear laid over plain floors) and the upright objects (treasury heaps,
// mushrooms, the core shard) in the high-res style: transparent pictures for their cells
// (cell, 64x64, 128x128)
macro_rules! hd_obj {
($id:expr, $name:literal) => {
($id, include_bytes!(concat!("../assets/textures/hd/obj-", $name, "-64.png")), include_bytes!(concat!("../assets/textures/hd/obj-", $name, "-128.png")))
};
}
const HD_DECALS: [(u32, &[u8], &[u8]); 8] = [
(D_RUBBLE, include_bytes!("../assets/textures/decal-rubble.png"), include_bytes!("../assets/textures/decal-rubble-128.png")),
(D_CRACK, include_bytes!("../assets/textures/decal-crack.png"), include_bytes!("../assets/textures/decal-crack-128.png")),
hd_obj!(D_COINS, "coins"),
hd_obj!(D_SILVER, "silver"),
hd_obj!(D_RUBY, "ruby"),
hd_obj!(D_DIAMOND, "diamond"),
hd_obj!(D_MUSHROOM, "mushroom"),
hd_obj!(D_SHARD, "shard"),
];
// the interface icons of the high-res style (art/generated/icons/): strips of icons, 64x64 and
// 128x128, for these cells in this order
macro_rules! hd_icons {
($name:literal, $cells:expr) => {
(include_bytes!(concat!("../assets/textures/icons/", $name, "-64.png")) as &[u8], include_bytes!(concat!("../assets/textures/icons/", $name, "-128.png")) as &[u8], $cells)
};
}
const HD_ICONS: [(&[u8], &[u8], &[u32]); 4] = [
hd_icons!("tools", &[I_PICK, I_HAND, I_SELL, I_FLAG, I_BOLT, I_HEAL, I_SUMMON, I_BRIDGE, I_WALL, I_STEEL]),
hd_icons!("towers", &[I_ARROW, I_CANNON, I_MAGIC, I_RUNE, I_DRAWBRIDGE, I_RAM, I_CATAPULT, I_ORDERS, I_ORDERS + 1, I_FREEZE]),
hd_icons!("traps", &[I_TRAPS, I_TRAPS + 1, I_TRAPS + 2, I_TRAPS + 3, I_TRAPS + 4, I_TRAPS + 5, I_TRAPS + 6, I_TRAPS + 7, I_TRAPS + 8, I_TRAPS + 9]),
hd_icons!("spells", &[I_SPELLS, I_SPELLS + 1, I_SPELLS + 2, I_SPELLS + 3, I_SPELLS + 4, I_SPELLS + 5, I_SPELLS + 6, I_SPELLS + 7]),
];
// Everything else in the world (see art/generated/textures/all/): a kind's own cells get as many
// variants as it has cells, tiles that fit together in any order (one seamless border, different
// middles), and a tile picks one by its position. (first cell, cells, 64x64 tiles side by side,
// 128x128 ones)
macro_rules! hd_var {
($id:expr, $n:expr, $name:literal) => {
($id, $n, include_bytes!(concat!("../assets/textures/hd/", $name, "-v64.png")), include_bytes!(concat!("../assets/textures/hd/", $name, "-v128.png")))
};
}
const HD_VARS: [(u32, u32, &[u8], &[u8]); 31] = [
hd_var!(T_FORTWALL, 4, "fortwall"),
hd_var!(T_STEEL, 4, "steel"),
hd_var!(T_OBSIDIAN, 4, "obsidian"),
hd_var!(T_FACE_EARTH, 1, "face"),
hd_var!(T_TREASURY, 4, "treasury"),
hd_var!(T_LAIR, 4, "lair"),
hd_var!(T_FARM, 4, "farm"),
hd_var!(T_TRAINING, 4, "training"),
hd_var!(T_LIBRARY, 4, "library"),
hd_var!(T_TORTURE, 4, "torture"),
hd_var!(T_TEMPLE, 4, "temple"),
hd_var!(T_PRISON, 4, "prison"),
hd_var!(T_GRAVEYARD, 4, "graveyard"),
hd_var!(T_GUARDPOST, 4, "guardpost"),
hd_var!(T_ARENA, 4, "arena"),
hd_var!(T_ALTAR, 4, "altar"),
hd_var!(T_FORGE, 4, "forge"),
hd_var!(T_AERIE, 4, "aerie"),
hd_var!(T_COREFLOOR, 4, "core"),
hd_var!(T_PORTALFLOOR, 4, "portal"),
hd_var!(T_GATEFLOOR, 4, "gate"),
hd_var!(T_HEROFLOOR, 4, "hero"),
hd_var!(T_BRIDGE, 2, "bridge"),
hd_var!(T_STONEBRIDGE, 2, "stonebridge"),
hd_var!(T_GRASS, 4, "grass"),
hd_var!(T_SNOW, 4, "snow"),
hd_var!(T_MOSS, 4, "moss"),
hd_var!(T_LEAVES, 2, "leaves"),
hd_var!(T_BARK, 1, "bark"),
hd_var!(T_STEM, 1, "stem"),
hd_var!(T_CAP, 1, "cap"),
];
const HD_SETS: [(u32, u32, u32, &[u8], &[u8], [u32; 5]); 12] = [
(T_CLAIM, 4, T_HDSET, include_bytes!("../assets/textures/floor-player.png"), include_bytes!("../assets/textures/floor-player-128.png"), [1; 5]),
(T_RIVAL1, 4, T_HDSET + 5, include_bytes!("../assets/textures/floor-mossgrim.png"), include_bytes!("../assets/textures/floor-mossgrim-128.png"), [1; 5]),
(T_RIVAL2, 4, T_HDSET + 10, include_bytes!("../assets/textures/floor-nightwell.png"), include_bytes!("../assets/textures/floor-nightwell-128.png"), [1; 5]),
(T_RIVAL3, 4, T_HDSET + 15, include_bytes!("../assets/textures/floor-saffrax.png"), include_bytes!("../assets/textures/floor-saffrax-128.png"), [1; 5]),
(T_FLOOR, 4, T_HDSET + 20, include_bytes!("../assets/textures/floor-v64.png"), include_bytes!("../assets/textures/floor-v128.png"), [1; 5]),
(T_EARTH, 4, T_HDSET + 25, include_bytes!("../assets/textures/earth-v64.png"), include_bytes!("../assets/textures/earth-v128.png"), [1; 5]),
(T_ROCK, 4, T_HDSET + 30, include_bytes!("../assets/textures/rock-v64.png"), include_bytes!("../assets/textures/rock-v128.png"), [1; 5]),
(T_GOLD, 4, T_HDSET + 35, include_bytes!("../assets/textures/gold-v64.png"), include_bytes!("../assets/textures/gold-v128.png"), [1; 5]),
(T_SILVER, 2, T_HDSET + 40, include_bytes!("../assets/textures/silver-v64.png"), include_bytes!("../assets/textures/silver-v128.png"), [1; 5]),
(T_RUBY, 2, T_HDSET + 45, include_bytes!("../assets/textures/ruby-v64.png"), include_bytes!("../assets/textures/ruby-v128.png"), [1; 5]),
(T_DIAMOND, 2, T_HDSET + 50, include_bytes!("../assets/textures/diamond-v64.png"), include_bytes!("../assets/textures/diamond-v128.png"), [1; 5]),
(T_REINF, 4, T_HDSET + 55, include_bytes!("../assets/textures/reinforced-v64.png"), include_bytes!("../assets/textures/reinforced-v128.png"), [1; 5]),
];
// the cell a tile at (x, z) shows in the high-res style: the kinds with a set pick one of its
// five tiles (HD_SETS), the liquids are laid out by the position (HD_TILES), everything else stays
pub fn hd_tile(id: u32, x: i32, z: i32) -> u32 {
if let Some(&(_, _, own, _, _, w)) = HD_SETS.iter().find(|f| (f.0..f.0 + f.1).contains(&id)) {
let h = (x as u32).wrapping_mul(73_856_093) ^ (z as u32).wrapping_mul(19_349_663);
let h = (h ^ (h >> 13)).wrapping_mul(0x5bd1_e995);
let mut r = (h >> 8) % w.iter().sum::<u32>();
for (k, &wk) in w.iter().enumerate() {
if r < wk {
return own + k as u32;
}
r -= wk;
}
}
if let Some(&(base, n, _, _)) = HD_VARS.iter().find(|&&(base, n, _, _)| (base..base + n).contains(&id)) {
let h = (x as u32).wrapping_mul(2_654_435_761) ^ (z as u32).wrapping_mul(40_503);
let h = (h ^ (h >> 15)).wrapping_mul(0x2c1b_3c6d);
return base + (h >> 9) % n;
}
match HD_TILES.iter().find(|&&(base, n, _, _)| (base..base + n).contains(&id)) {
Some(&(base, 4, _, _)) => base + (x & 1) as u32 + (z & 1) as u32 * 2,
_ => id,
}
}
// mean and standard deviation per channel of the opaque texels of some cells
fn cell_stats(img: &Image, cells: &[u32], tile: u32) -> ([f32; 3], [f32; 3]) {
let (mut sum, mut sq, mut n) = ([0f32; 3], [0f32; 3], 0f32);
for &id in cells {
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
for y in 0..tile {
for x in 0..tile {
let p = img.get_pixel(ox + x, oy + y);
if p.a < 0.5 {
continue;
}
for (c, v) in [p.r, p.g, p.b].into_iter().enumerate() {
sum[c] += v;
sq[c] += v * v;
}
n += 1.0;
}
}
}
let n = n.max(1.0);
let mean = sum.map(|s| s / n);
let mut sd = [0f32; 3];
for c in 0..3 {
sd[c] = (sq[c] / n - mean[c] * mean[c]).max(0.0).sqrt();
}
(mean, sd)
}
// moves the colours of the high-res cells `dst` towards those of the 16x16 cells `src`
fn match_colors(img: &mut Image, small: &Image, src: &[u32], dst: &[u32], tile: u32) {
const STRENGTH: f32 = 0.8;
let (pm, ps) = cell_stats(small, src, 16);
let (hm, hs) = cell_stats(img, dst, tile);
for &id in dst {
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
for y in 0..tile {
for x in 0..tile {
let p = img.get_pixel(ox + x, oy + y);
if p.a < 0.5 {
continue;
}
let mut q = [p.r, p.g, p.b];
for c in 0..3 {
let k = (ps[c] / hs[c].max(0.01)).clamp(0.6, 1.6);
let m = pm[c] + (q[c] - hm[c]) * k;
q[c] = (q[c] + (m - q[c]) * STRENGTH).clamp(0.0, 1.0);
}
img.set_pixel(ox + x, oy + y, Color::new(q[0], q[1], q[2], p.a));
}
}
}
}
// a high-res atlas with `tile` x `tile` texel cells (HD_TILE or HD_TILE2)
fn gen_hd_atlas(tile: u32) -> Image {
let small = gen_atlas();
let k = tile / 16;
let (w, h) = (ATLAS_COLS * tile, ATLAS_ROWS * tile);
let mut img = Image::gen_image_color(w as u16, h as u16, Color::new(0.0, 0.0, 0.0, 0.0));
for y in 0..h {
for x in 0..w {
img.set_pixel(x, y, small.get_pixel(x / k, y / k));
}
}
for &(base, n, png, png2) in HD_TILES.iter() {
let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd tile");
for v in 0..n {
let (sx, sy) = if n == 4 { ((v % 2) * tile, (v / 2) * tile) } else { (0, 0) };
let id = base + v;
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
for y in 0..tile {
for x in 0..tile {
img.set_pixel(ox + x, oy + y, src.get_pixel(sx + x, sy + y));
}
}
}
}
for &(_, _, own, png, png2, _) in HD_SETS.iter() {
let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd set");
for v in 0..5 {
let id = own + v;
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
for y in 0..tile {
for x in 0..tile {
img.set_pixel(ox + x, oy + y, src.get_pixel(v * tile + x, y));
}
}
}
}
for &(base, n, png, png2) in HD_VARS.iter() {
let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd variants");
for v in 0..n {
let id = base + v;
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
for y in 0..tile {
for x in 0..tile {
img.set_pixel(ox + x, oy + y, src.get_pixel(v * tile + x, y));
}
}
}
}
for &(id, png, png2) in HD_DECALS.iter() {
let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd decal");
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
for y in 0..tile {
for x in 0..tile {
img.set_pixel(ox + x, oy + y, src.get_pixel(x, y));
}
}
}
for &(png, png2, cells) in HD_ICONS.iter() {
let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd icons");
for (v, &id) in cells.iter().enumerate() {
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
for y in 0..tile {
for x in 0..tile {
img.set_pixel(ox + x, oy + y, src.get_pixel(v as u32 * tile + x, y));
}
}
}
}
// the generated pictures take on the colours of the 16x16 cells they replace (mean and
// contrast per channel, most of the way), so switching between the styles keeps the look
let range = |a: u32, n: u32| (a..a + n).collect::<Vec<u32>>();
let mut groups: Vec<(Vec<u32>, Vec<u32>)> = Vec::new();
for &(base, n, _, _) in HD_TILES.iter() {
groups.push((range(base, n), range(base, n)));
}
for &(first, n, own, _, _, _) in HD_SETS.iter() {
groups.push((range(first, n), range(own, 5)));
}
for &(base, n, _, _) in HD_VARS.iter() {
groups.push((range(base, n), range(base, n)));
}
for &(id, _, _) in HD_DECALS.iter().skip(2) {
groups.push((vec![id], vec![id]));
}
for (src, dst) in groups.iter() {
match_colors(&mut img, &small, src, dst, tile);
}
img
}
fn gen_atlas() -> Image {
let mut cv = Canvas { img: Image::gen_image_color((ATLAS_COLS * 16) as u16, (ATLAS_ROWS * 16) as u16, Color::new(0.0, 0.0, 0.0, 0.0)) };
for v in 0..4u32 {
// rock
cv.cell(T_ROCK + v, |x, y| {
let blk = rnd(x / 4 + (y / 4) % 2, y / 4, v + 21) * 20.0 - 10.0;
let fine = rnd(x, y, v + 5) * 10.0 - 5.0;
let crack = (x + y * 3 + v as i32 * 5) % 23 == 0 || (x * 2 - y + v as i32) % 31 == 0;
if crack {
Some(c(40, 40, 48))
} else {
Some(shade((80, 80, 92), (blk + fine) as i32))
}
});
// earth
cv.cell(T_EARTH + v, |x, y| Some(earth_px(x, y, v)));
// gold
cv.cell(T_GOLD + v, |x, y| {
let mut best = 99.0f32;
for k in 0..6 {
let nx = 2 + (hash(k, 1, v + 40) % 12) as i32;
let ny = 2 + (hash(k, 2, v + 40) % 12) as i32;
let d = (((x - nx) * (x - nx) + (y - ny) * (y - ny)) as f32).sqrt();
best = best.min(d - if k % 2 == 0 { 0.5 } else { 0.0 });
}
if best < 0.6 {
Some(c(255, 240, 150))
} else if best < 1.4 {
Some(c(240, 196, 52))
} else if best < 2.0 {
Some(c(168, 118, 30))
} else {
Some(earth_px(x, y, v + 60))
}
});
cv.cell(T_FLOOR + v, |x, y| Some(stones(x, y, v, (64, 56, 50), (34, 30, 28), 10)));
cv.cell(T_CLAIM + v, |x, y| {
let col = stones(x, y, v + 10, (78, 44, 58), (36, 20, 28), 10);
// small emblem in the middle of every other stone
if (x % 8 == 4 && y % 8 == 4) && hash(x / 8, y / 8, v) % 2 == 0 {
Some(c(150, 60, 80))
} else {
Some(col)
}
});
cv.cell(T_HEROFLOOR + v, |x, y| Some(stones(x, y, v + 20, (132, 136, 150), (80, 84, 96), 12)));
cv.cell(T_TREASURY + v, |x, y| {
let chk = ((x / 4) + (y / 4)) % 2 == 0;
let base = if chk { (120, 92, 36) } else { (100, 74, 30) };
let d = (rnd(x, y, v + 30) * 10.0) as i32 - 5;
if x % 4 == 0 || y % 4 == 0 {
Some(shade((70, 52, 22), d))
} else {
Some(shade(base, d))
}
});
cv.cell(T_LAIR + v, |x, y| {
let streak = ((x * 2 + y + (hash(y, 0, v) % 3) as i32) % 5) as i32;
let d = (rnd(x, y, v + 31) * 18.0) as i32 - 9 + streak * 4;
Some(shade((112, 86, 46), d))
});
cv.cell(T_FARM + v, |x, y| {
let moss = rnd(x / 2, y / 2, v + 32);
let d = (rnd(x, y, v + 33) * 14.0) as i32 - 7;
if moss > 0.55 {
Some(shade((58, 98, 40), d))
} else {
Some(shade((62, 46, 32), d))
}
});
cv.cell(T_TRAINING + v, |x, y| {
let plank = y / 4;
let off = (hash(plank, 0, v + 34) % 16) as i32;
if y % 4 == 0 || (x + off) % 16 == 0 {
Some(c(58, 38, 24))
} else {
let grain = ((x + plank * 5) % 7 == 0) as i32 * -10;
Some(shade((116, 80, 48), (rnd(x, y, v + 35) * 10.0) as i32 + grain))
}
});
cv.cell(T_LIBRARY + v, |x, y| {
if x >= 3 && x <= 12 && y >= 3 && y <= 12 {
let border = x == 3 || x == 12 || y == 3 || y == 12;
if border {
Some(c(170, 130, 50))
} else {
Some(shade((116, 30, 42), (rnd(x, y, v + 36) * 10.0) as i32))
}
} else {
let plank = x / 4;
if x % 4 == 0 {
Some(c(40, 24, 16))
} else {
Some(shade((74, 46, 32), (rnd(x, y, v + 37) * 10.0) as i32 + (plank % 2) * 4))
}
}
});
cv.cell(T_COREFLOOR + v, |x, y| Some(stones(x, y, v + 40, (86, 32, 34), (40, 14, 16), 10)));
cv.cell(T_PORTALFLOOR + v, |x, y| Some(stones(x, y, v + 50, (66, 44, 92), (32, 20, 46), 10)));
cv.cell(T_GATEFLOOR + v, |x, y| {
let vein = ((x as f32 * 0.7 + (y as f32 * 0.5).sin() * 3.0 + v as f32 * 4.0) as i32) % 9 == 0;
if vein {
Some(c(150, 150, 165))
} else {
Some(shade((196, 196, 206), (rnd(x, y, v + 60) * 12.0) as i32 - 6))
}
});
}
// wall faces
cv.cell(T_FACE_EARTH, |x, y| {
let streak = (hash(x, 0, 70) % 3) as i32 * 5;
Some(shade((62, 42, 28), (rnd(x, y, 71) * 10.0) as i32 - streak - y / 2))
});
cv.cell(T_FACE_ROCK, |x, y| {
let streak = (hash(x, 0, 72) % 3) as i32 * 5;
Some(shade((54, 54, 64), (rnd(x, y, 73) * 10.0) as i32 - streak - y / 2))
});
cv.cell(T_FACE_GOLD, |x, y| {
if hash(x, y, 74) % 9 == 0 {
Some(c(200, 160, 40))
} else {
Some(shade((62, 42, 28), (rnd(x, y, 75) * 10.0) as i32 - y / 2))
}
});
// dig marker: diagonal stripes
cv.cell(T_MARK, |x, y| {
if (x + y) % 6 < 2 {
Some(Color::from_rgba(255, 220, 60, 150))
} else if x == 0 || y == 0 || x == 15 || y == 15 {
Some(Color::from_rgba(255, 220, 60, 200))
} else {
None
}
});
// bed
cv.cell(D_BED, |x, y| {
let dx = (x as f32 - 7.5) / 6.5;
let dy = (y as f32 - 8.0) / 5.5;
let d = dx * dx + dy * dy;
if d < 0.55 {
Some(if x < 5 { c(200, 190, 160) } else { c(110, 50, 60) })
} else if d < 1.0 {
Some(c(150, 118, 64))
} else {
None
}
});
// mushrooms
cv.cell(D_MUSHROOM, |x, y| {
let caps = [(4, 6, 3), (11, 5, 2), (8, 11, 3)];
for &(cx, cy, r) in &caps {
let dx = x - cx;
let dy = y - cy;
if dy <= 0 && dx * dx + dy * dy * 2 <= r * r + 1 {
return Some(if (x + y) % 3 == 0 && dy < 0 { c(250, 230, 220) } else { c(200, 40, 50) });
}
if dx.abs() <= 0 && dy > 0 && dy <= r {
return Some(c(230, 220, 200));
}
}
None
});
// training dummy
cv.cell(D_DUMMY, |x, y| {
if (x == 7 || x == 8) && y >= 4 {
Some(c(90, 60, 36))
} else if y == 7 && (3..=12).contains(&x) {
Some(c(90, 60, 36))
} else if (x - 7).pow(2) + (y - 3).pow(2) <= 5 {
Some(c(200, 170, 110))
} else if y >= 14 && (4..=11).contains(&x) {
Some(c(60, 40, 24))
} else {
None
}
});
// gold pile
cv.cell(D_COINS, |x, y| {
let dx = (x as f32 - 7.5) / 7.0;
let dy = (y as f32 - 11.0) / 5.0;
let d = dx * dx + dy * dy;
if d < 1.0 && y <= 14 {
let hl = hash(x, y, 80) % 5 == 0;
Some(if hl { c(255, 245, 170) } else if d > 0.6 { c(190, 140, 30) } else { c(242, 200, 60) })
} else {
None
}
});
// bookshelf
cv.cell(D_SHELF, |x, y| {
if y > 9 || x == 0 || x == 15 {
return if y <= 10 { Some(c(60, 36, 22)) } else { None };
}
if y == 0 || y == 5 {
return Some(c(60, 36, 22));
}
let book = hash(x, y / 5, 81) % 5;
Some(match book {
0 => c(150, 40, 40),
1 => c(40, 80, 140),
2 => c(60, 120, 60),
3 => c(160, 130, 50),
_ => c(100, 60, 120),
})
});
// reinforced walls: masonry in dark stone with the player's red mortar
for v in 0..4u32 {
// reinforced wall: a castle wall of roughly squared grey blocks in regular courses (three
// courses of 6, 5 and 5 pixels, every course cut into blocks of 5-9 pixels at its own places;
// wrapped, so the cell is seamless), dark mortar with a crimson tint, the top of every
// block lit, its bottom in shadow, now and then a chipped corner
cv.cell(T_REINF + v, |x, y| {
let (row, ry, rh) = match y {
0..=5 => (0, y, 6),
6..=10 => (1, y - 6, 5),
_ => (2, y - 11, 5),
};
// the course's cuts: from an offset on, blocks of 5-9 pixels around the cell
let off = (hash(row, 1, v + 95) % 6) as i32;
let mut cuts = vec![off];
let mut c = off;
let mut k = 0;
loop {
c += 5 + (hash(row, k, v + 96) % 5) as i32;
k += 1;
if c > off + 16 - 4 {
break;
}
cuts.push(c);
}
let xs = (x - off).rem_euclid(16) + off;
let block = cuts.iter().filter(|&&q| q <= xs).count() as i32;
let start = cuts[(block - 1).max(0) as usize];
let end = cuts.get(block as usize).copied().unwrap_or(off + 16);
let mortar = ry == rh - 1 || xs == end - 1;
let chip = (xs == start || xs == end - 2) && (ry == 0 || ry == rh - 2) && hash(row * 7 + block, ry, v + 97) % 4 == 0;
if mortar || chip {
return Some(shade((46, 30, 34), (rnd(x, y, v + 98) * 8.0) as i32));
}
let tone = (hash(row * 11 + block, 3, v + 99) % 5) as i32 * 5 - 10;
let face = if ry == 0 {
12
} else if ry == rh - 2 {
-8
} else {
0
};
Some(shade((92, 90, 96), tone + face + (rnd(x, y, v + 100) * 10.0) as i32 - 5))
});
// torture chamber: dark stones with rusty iron grates
cv.cell(T_TORTURE + v, |x, y| {
let grate = (4..=11).contains(&x) && (4..=11).contains(&y) && (x % 3 == 1 || y % 3 == 1);
if grate {
return Some(shade((96, 44, 30), (rnd(x, y, v + 93) * 12.0) as i32));
}
Some(stones(x, y, v + 94, (52, 40, 44), (26, 16, 18), 10))
});
}
// temple: dark marble with a glowing red-gold rune circle
for v in 0..4u32 {
cv.cell(T_TEMPLE + v, |x, y| {
let (dx, dy) = (x as f32 - 7.5, y as f32 - 7.5);
let d = (dx * dx + dy * dy).sqrt();
if (5.0..6.0).contains(&d) {
return Some(c(170, 70, 40));
}
if d < 1.5 {
return Some(c(220, 150, 60));
}
let vein = ((x as f32 * 0.6 + (y as f32 * 0.7).sin() * 2.0 + v as f32 * 3.0) as i32) % 7 == 0;
Some(if vein { c(70, 54, 66) } else { shade((44, 34, 46), (rnd(x, y, v + 95) * 10.0) as i32) })
});
}
// altar with a flame
cv.cell(D_ALTAR, |x, y| {
if y >= 9 && (3..=12).contains(&x) {
Some(if y == 9 { c(150, 140, 150) } else { c(100, 92, 104) })
} else if y < 9 && y >= 3 && (x - 7).abs() + (y - 8).abs() / 2 <= 3 {
Some(if (x + y) % 3 == 0 { c(255, 220, 80) } else { c(255, 120, 30) })
} else {
None
}
});
// torture wheel
cv.cell(D_RACK, |x, y| {
let (dx, dy) = (x as f32 - 7.5, y as f32 - 7.0);
let d = (dx * dx + dy * dy).sqrt();
let a = dy.atan2(dx);
if (5.5..7.0).contains(&d) || (d < 6.0 && (a * 4.0 / std::f32::consts::PI).fract().abs() < 0.12) {
Some(c(110, 76, 44))
} else if y >= 13 && (x == 7 || x == 8 || (4..=11).contains(&x) && y == 15) {
Some(c(70, 48, 30))
} else {
None
}
});
// tool icons
cv.cell(I_PICK, |x, y| {
let handle = (x + y == 16 || x + y == 17) && x >= 3;
let head = (y == 3 || y == 4) && (2..=13).contains(&x) && (x - 8).abs() + (y - 2) * 2 < 9;
if head {
Some(c(200, 205, 215))
} else if handle {
Some(c(140, 90, 50))
} else {
None
}
});
cv.cell(I_HAND, |x, y| {
let palm = (3..=12).contains(&x) && (7..=14).contains(&y);
let fingers = y >= 2 && y < 8 && (x == 4 || x == 6 || x == 8 || x == 10) && !(x == 10 && y < 4);
let thumb = (x == 13 || x == 14) && (6..=10).contains(&y);
if palm || fingers || thumb {
Some(c(220, 180, 150))
} else {
None
}
});
cv.cell(I_SELL, |x, y| {
let d = (x - 8) * (x - 8) + (y - 8) * (y - 8);
if d <= 36 && d >= 20 {
Some(c(240, 200, 60))
} else if (x == 8 && (4..=12).contains(&y)) || (y == 8 && (5..=11).contains(&x)) {
Some(c(240, 200, 60))
} else {
None
}
});
cv.cell(I_FLAG, |x, y| {
if x == 3 && y >= 1 {
Some(c(180, 180, 180))
} else if x > 3 && x < 14 && y >= 2 && y < 9 - ((x - 4) / 3) % 2 {
Some(c(210, 40, 40))
} else {
None
}
});
cv.cell(I_BOLT, |x, y| {
let line1 = y < 8 && (x - (10 - y / 2)).abs() <= 1;
let line2 = y >= 7 && y <= 9 && (5..=11).contains(&x);
let line3 = y > 8 && (x - (8 - (y - 9) / 2)).abs() <= 1;
if line1 || line2 || line3 {
Some(c(255, 240, 120))
} else {
None
}
});
cv.cell(I_HEAL, |x, y| {
if ((6..=9).contains(&x) && (2..=13).contains(&y)) || ((2..=13).contains(&x) && (6..=9).contains(&y)) {
Some(c(90, 230, 110))
} else {
None
}
});
cv.cell(I_ARROW, |x, y| {
// a small wooden tower with an arrow
if (4..=11).contains(&x) && y >= 7 {
Some(if (x + y) % 4 == 0 { c(90, 60, 34) } else { c(130, 88, 50) })
} else if y == 6 && x % 2 == 0 && (4..=11).contains(&x) {
Some(c(130, 88, 50))
} else if y == 3 && (2..=13).contains(&x) || (x == 12 && (y == 2 || y == 4)) {
Some(c(220, 220, 220))
} else {
None
}
});
cv.cell(I_CANNON, |x, y| {
let barrel = y >= 4 && y <= 7 && x >= 5 && x <= 14 && (y - 4) as i32 <= 3;
let wheel = (x - 6) * (x - 6) + (y - 10) * (y - 10) <= 9;
if barrel {
Some(c(50, 50, 56))
} else if wheel {
Some(c(120, 80, 40))
} else {
None
}
});
cv.cell(I_MAGIC, |x, y| {
let crystal = (x - 8).abs() + (y - 5).abs() <= 3;
if crystal {
Some(c(190, 110, 255))
} else if (6..=9).contains(&x) && y >= 9 {
Some(c(110, 110, 124))
} else {
None
}
});
cv.cell(I_RUNE, |x, y| {
// a stone obelisk with a glowing rune, bronze bands
let body = (5..=10).contains(&x) && y >= 3 && y <= 14 && !(y == 3 && (x == 5 || x == 10));
if !body {
return None;
}
let rune = (x == 7 || x == 8) && (6..=10).contains(&y) || (y == 8 && (6..=9).contains(&x));
Some(if rune {
c(90, 210, 255)
} else if y == 4 || y == 12 {
c(190, 120, 60)
} else if (x + y) % 5 == 0 {
c(80, 80, 92)
} else {
c(110, 110, 124)
})
});
cv.cell(I_SUMMON, |x, y| {
let d = (((x - 8) * (x - 8) + (y - 8) * (y - 8)) as f32).sqrt();
let a = ((y - 8) as f32).atan2((x - 8) as f32);
if d < 7.0 && ((a * 3.0 + d * 0.9).sin() > 0.3) {
Some(c(200, 90, 230))
} else {
None
}
});
// ---- campaign terrain
for v in 0..4u32 {
// water: dark blue with lighter wave crests. Seamless (period 16): the shader lets it flow
// across the tiles, sampled in world space
cv.cell(T_WATER + v, |x, y| {
let tau = std::f32::consts::TAU;
let bend = ((y as f32 + v as f32 * 3.0) * tau / 16.0).sin() * 1.6 + ((x as f32) * tau / 8.0).sin() * 0.6;
let wave = (x + y * 2 + bend.round() as i32).rem_euclid(8);
let d = (rnd(x, y, v + 120) * 10.0) as i32 - 5;
Some(match wave {
0 => shade((70, 120, 170), d),
1 => shade((50, 92, 140), d),
_ => shade((24, 50, 88), d),
})
});
// lava: molten orange with hot yellow streaks and darker red troughs, no black crust.
// Seamless (period 16) so it can flow across the tiles
cv.cell(T_LAVA + v, |x, y| {
let tau = std::f32::consts::TAU;
let (fx, fy) = (x as f32 / 16.0, y as f32 / 16.0);
let o = v as f32 * 1.3;
let heat = (tau * (fx + fy * 2.0) + o).sin() * 0.45
+ (tau * (fx * 3.0 - fy) + (tau * fy * 2.0 + o).sin() * 1.2).sin() * 0.35
+ (tau * (fy * 3.0 + fx * 2.0) + o * 2.0).sin() * 0.2;
let d = (rnd(x, y, v + 134) * 16.0) as i32 - 8;
Some(if heat > 0.62 {
shade((255, 228, 120), d / 2)
} else if heat > 0.3 {
shade((255, 170, 55), d)
} else if heat > -0.35 {
shade((240, 110, 25), d)
} else if heat > -0.7 {
shade((200, 64, 18), d)
} else {
shade((150, 40, 14), d)
})
});
// knight fortress walls: pale stone blocks with blue-grey mortar
cv.cell(T_FORTWALL + v, |x, y| {
let row = y / 5;
let off = if row % 2 == 1 { 5 } else { 0 };
if y % 5 == 4 || (x + off) % 10 == 9 {
return Some(shade((70, 76, 92), (rnd(x, y, v + 140) * 8.0) as i32));
}
let blk = hash((x + off) / 10, row, v + 141) % 4;
Some(shade((150, 150, 160), (rnd(x, y, v + 142) * 12.0) as i32 - 6 + blk as i32 * 5 - 8 + if y % 5 == 0 { 10 } else { 0 }))
});
}
for v in 0..2u32 {
// wooden bridge: planks across, two rope rails at the sides
cv.cell(T_BRIDGE + v, |x, y| {
if x <= 1 || x >= 14 {
return Some(if (y + v as i32) % 4 == 0 { c(150, 120, 80) } else { c(96, 70, 42) });
}
if y % 4 == 0 {
return Some(c(40, 26, 16));
}
let plank = y / 4;
let off = (hash(plank, 1, v + 150) % 16) as i32;
if (x + off) % 13 == 0 && y % 4 == 2 {
return Some(c(60, 40, 24)); // nail
}
Some(shade((126, 88, 52), (rnd(x, y, v + 151) * 14.0) as i32 - 7 + (plank % 2) * 6))
});
// stone bridge over lava: big grey slabs, sooty edges
cv.cell(T_STONEBRIDGE + v, |x, y| {
if x <= 1 || x >= 14 {
return Some(shade((60, 56, 56), (rnd(x, y, v + 160) * 10.0) as i32));
}
Some(stones(x, y, v + 161, (104, 98, 96), (46, 40, 40), 12))
});
}
// shard of the soul core: a glowing red crystal
cv.cell(D_SHARD, |x, y| {
let (dx, dy) = ((x - 8).abs(), y);
if y >= 2 && y <= 14 && dx * 3 <= (if y < 7 { (y - 2) * 4 } else { (15 - y) * 2 }) {
Some(if dx == 0 || (x + y) % 5 == 0 { c(255, 170, 170) } else if x < 8 { c(230, 50, 60) } else { c(160, 20, 40) })
} else if dy == 15 && (4..=12).contains(&x) {
Some(c(80, 30, 40))
} else {
None
}
});
// knight banner: pole with a white-blue banner and a golden sun
cv.cell(D_BANNER, |x, y| {
if x == 3 {
return Some(c(170, 170, 180));
}
if (4..=13).contains(&x) && (1..=11).contains(&y) {
let (dx, dy) = (x - 9, y - 6);
if dx * dx + dy * dy <= 5 {
return Some(c(240, 200, 60));
}
return Some(if y == 11 && x % 2 == 0 { c(60, 90, 170) } else if x % 5 == 4 { c(200, 205, 220) } else { c(230, 232, 240) });
}
None
});
// bridge tool icon: an arch with planks
cv.cell(I_BRIDGE, |x, y| {
let (dx, dy) = (x as f32 - 7.5, y as f32 - 13.0);
let d = (dx * dx + dy * dy).sqrt();
if y <= 6 && y >= 5 && (1..=14).contains(&x) {
Some(if x % 3 == 0 { c(70, 46, 26) } else { c(150, 106, 60) })
} else if (6.0..7.5).contains(&d) && y >= 6 {
Some(c(120, 84, 48))
} else if y > 6 && (x == 1 || x == 14) {
Some(c(110, 76, 44))
} else if y >= 12 && d >= 7.5 && (2..=13).contains(&x) {
Some(c(60, 110, 170))
} else {
None
}
});
// drawbridge tool icon: a raised flap with its chains above a river
cv.cell(I_DRAWBRIDGE, |x, y| {
if (3..=5).contains(&x) && (2..=12).contains(&y) {
Some(if y % 3 == 0 { c(70, 46, 26) } else { c(150, 106, 60) })
} else if y == 12 && (5..=13).contains(&x) {
Some(c(110, 76, 44))
} else if (x - 5) * 2 == (y - 2) * 1 + 2 && (6..=12).contains(&x) {
Some(c(170, 170, 180)) // chain
} else if x == 13 && (3..=12).contains(&y) {
Some(c(90, 90, 100)) // post
} else if y >= 13 && (1..=14).contains(&x) {
Some(c(60, 110, 170))
} else {
None
}
});
// wall tool icon: a block of bricks with an arrow up
cv.cell(I_WALL, |x, y| {
if (3..=12).contains(&x) && (6..=14).contains(&y) {
let row = (y - 6) / 3;
let joint = (y - 6) % 3 == 2 || (x + row * 3) % 6 == 0;
Some(if joint { c(60, 40, 30) } else { c(150, 100, 70) })
} else if y <= 4 && (x - 8).abs() <= 4 - y.max(1) + 1 && y >= 1 {
Some(c(200, 140, 255))
} else if y == 5 && (7..=9).contains(&x) {
Some(c(200, 140, 255))
} else {
None
}
});
// siege icons: a ram (a trunk with an iron cap under a roof), a catapult (arm and stone)
cv.cell(I_RAM, |x, y| {
if (3..=12).contains(&x) && y == 4 + (x - 3).min(12 - x).min(3) * 0 && false {
None
} else if y >= 3 && y <= 5 && (x - 7).abs() <= 6 - (y - 3) * 0 && (x + y) % 2 == 0 {
Some(c(92, 58, 32))
} else if (7..=9).contains(&y) && (1..=13).contains(&x) {
Some(if x <= 3 { c(170, 175, 190) } else { c(140, 92, 52) })
} else if y >= 11 && y <= 13 && ((x - 4).abs() <= 1 || (x - 11).abs() <= 1) {
Some(c(30, 24, 26))
} else if y == 10 && (3..=12).contains(&x) {
Some(c(92, 58, 32))
} else {
None
}
});
cv.cell(I_CATAPULT, |x, y| {
let arm = x + y == 14 && (3..=11).contains(&x);
if (x - 12).pow(2) + (y - 3).pow(2) <= 3 {
Some(c(150, 152, 140))
} else if arm {
Some(c(140, 92, 52))
} else if y == 11 && (2..=13).contains(&x) {
Some(c(92, 58, 32))
} else if y >= 12 && ((x - 4).abs() <= 1 || (x - 11).abs() <= 1) {
Some(c(30, 24, 26))
} else {
None
}
});
// frost spell icon: a snowflake
cv.cell(I_FREEZE, |x, y| {
let (dx, dy) = (x - 8, y - 8);
let arm = dx == 0 || dy == 0 || dx == dy || dx == -dy;
let r = dx.abs().max(dy.abs());
if arm && r <= 6 {
Some(if r <= 1 { c(240, 250, 255) } else { c(150, 210, 255) })
} else if r == 4 && (dx.abs() == 1 || dy.abs() == 1) && (dx == 0 || dy == 0 || dx.abs() == dy.abs() + 1 || dy.abs() == dx.abs() + 1) {
Some(c(200, 235, 255))
} else {
None
}
});
// giant mushroom: pale ribbed stem, a violet cap with glowing spots
cv.cell(T_STEM, |x, y| {
let r = hash(x, y, 410);
Some(if x % 4 == 0 { c(150, 140, 130) } else if r % 9 == 0 { c(170, 160, 150) } else { c(200, 192, 178) })
});
cv.cell(T_CAP, |x, y| {
let spot = |cx: i32, cy: i32| (x - cx).pow(2) + (y - cy).pow(2) <= 2;
if spot(4, 4) || spot(11, 6) || spot(6, 11) || spot(12, 12) {
Some(c(150, 255, 230))
} else if (x + y) % 5 == 0 {
Some(c(90, 50, 130))
} else {
Some(c(120, 66, 170))
}
});
// tree: bark and two kinds of crown
cv.cell(T_BARK, |x, y| {
let r = hash(x, y, 420);
Some(if x % 5 == 0 || r % 7 == 0 { c(60, 40, 24) } else { c(96, 66, 40) })
});
for v in 0..2u32 {
cv.cell(T_LEAVES + v, |x, y| {
let r = hash(x, y, 430 + v);
// dark, bluish green: clearly apart from the light meadow
Some(if r % 5 == 0 { c(14, 40, 30) } else if r % 7 == 0 { c(52, 96, 62) } else if v == 0 { c(26, 64, 44) } else { c(32, 72, 40) })
});
}
// snow above ground in winter
for v in 0..4u32 {
cv.cell(T_SNOW + v, |x, y| {
let r = hash(x, y, 450 + v);
Some(if r % 9 == 0 { c(214, 222, 236) } else if r % 23 == 0 { c(150, 170, 120) } else { c(236, 240, 248) })
});
}
// a carved pumpkin: orange ribs, a grinning face
cv.cell(T_PUMPKIN, |x, y| {
let eye = (y == 5 || y == 6) && (x == 4 || x == 5 || x == 10 || x == 11);
let mouth = (y == 10 && (3..=12).contains(&x)) || (y == 11 && (5..=10).contains(&x) && x % 2 == 0);
if eye || mouth {
Some(c(255, 230, 90))
} else if x % 4 == 0 {
Some(c(190, 90, 20))
} else {
Some(c(240, 128, 30))
}
});
// Christmas: a present in red paper with a golden ribbon, fir needles with baubles
cv.cell(T_GIFT, |x, y| {
if x == 7 || x == 8 || y == 7 || y == 8 {
Some(c(250, 210, 80))
} else if (x + y) % 5 == 0 {
Some(c(200, 30, 40))
} else {
Some(c(176, 24, 34))
}
});
cv.cell(T_FIR, |x, y| {
let r = hash(x, y, 470);
if r % 29 == 0 {
Some([c(240, 60, 60), c(250, 210, 80), c(90, 160, 255)][(r / 29 % 3) as usize])
} else if (x + 2 * y) % 5 == 0 {
Some(c(20, 70, 40))
} else {
Some(shade((34, 104, 56), (rnd(x, y, 471) * 16.0) as i32 - 8))
}
});
// Easter: painted eggs, stripes and zigzags in spring colours
for v in 0..2u32 {
cv.cell(T_EGG + v, |x, y| {
let cols = if v == 0 { [c(140, 200, 250), c(250, 230, 120), c(255, 255, 255)] } else { [c(250, 160, 200), c(150, 220, 140), c(255, 250, 230)] };
let zig = (y + (x % 4 - 2).abs()) % 8;
Some(match zig {
0 | 1 => cols[1],
4 => cols[2],
_ => cols[0],
})
});
}
// grass above ground
for v in 0..4u32 {
cv.cell(T_GRASS + v, |x, y| {
let r = hash(x, y, 440 + v);
// a light, yellowish meadow with flowers
Some(if r % 11 == 0 { c(176, 200, 96) } else if r % 6 == 0 { c(112, 150, 62) } else if r % 23 == 0 { c(236, 220, 110) } else if r % 37 == 0 { c(230, 236, 240) } else { c(140, 176, 78) })
});
}
// mushroom grove: dark stones overgrown with teal moss, a few glowing spores
for v in 0..4u32 {
cv.cell(T_MOSS + v, |x, y| {
let r = hash(x, y, 520 + v);
let moss = (hash(x / 3, y / 3, 530 + v) % 5) < 3;
Some(if r % 41 == 0 {
c(150, 255, 220) // a spore glows
} else if r % 29 == 0 {
c(200, 150, 255)
} else if moss {
if r % 4 == 0 { c(46, 112, 96) } else if r % 3 == 0 { c(30, 84, 74) } else { c(38, 98, 84) }
} else {
stones(x, y, v, (52, 58, 56), (26, 30, 30), 8)
})
});
}
// obsidian: cooled lava, dark glass with a few glowing seams
for v in 0..4u32 {
cv.cell(T_OBSIDIAN + v, |x, y| {
let r = hash(x, y, v + 400);
if (x * 5 + y * 3 + v as i32 * 7) % 29 == 0 || ((x + 2 * y + v as i32) % 23 == 0 && r % 2 == 0) {
Some(c(230, 90, 30))
} else if r % 11 == 0 {
Some(c(70, 60, 90))
} else if (x + y) % 7 == 0 {
Some(c(34, 26, 40))
} else {
Some(c(22, 16, 26))
}
});
}
// ---- more treasures: veins (earth with embedded pieces) and treasury piles
let vein = |x: i32, y: i32, v: u32, seed: u32, hi: Color, mid: Color, lo: Color, size: f32| -> Color {
let mut best = 99.0f32;
for k in 0..5 {
let nx = 2 + (hash(k, 1, v + seed) % 12) as i32;
let ny = 2 + (hash(k, 2, v + seed) % 12) as i32;
best = best.min((((x - nx).abs() + (y - ny).abs()) as f32) - if k % 2 == 0 { 0.5 } else { 0.0 });
}
if best < size * 0.4 {
hi
} else if best < size {
mid
} else if best < size + 0.8 {
lo
} else {
earth_px(x, y, v + seed)
}
};
for v in 0..2u32 {
cv.cell(T_SILVER + v, |x, y| Some(vein(x, y, v, 170, c(245, 245, 255), c(180, 186, 200), c(100, 104, 118), 2.0)));
cv.cell(T_RUBY + v, |x, y| Some(vein(x, y, v, 180, c(255, 150, 170), c(210, 30, 60), c(110, 16, 36), 1.6)));
cv.cell(T_DIAMOND + v, |x, y| Some(vein(x, y, v, 190, c(255, 255, 255), c(150, 230, 255), c(70, 140, 190), 1.4)));
}
// treasury heaps, same dome shape as the gold pile: silver bars, rubies, diamonds
let heap = |x: i32, y: i32| -> Option<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/imp-sprite and art/generated/sprites), used
// instead of the 10x10 ones in the high-res graphics style (render3d::billboards). Two levels of
// detail from the same pictures, in two textures: 64x64 when the sprite gets enough screen
// pixels (close), 32x32 otherwise, so neither loses pixels nor flickers. Sheets: transparent,
// the feet on the bottom row; idle: the five views side by side (VIEWS); an animation:
// ANIM_FRAMES frames side by side, one row per view. The gear (shoulder pieces, helmet, cape,
// crown) is laid over them at draw time (see hd_anchor, GEAR_PARTS); the hit flash is drawn
// white by the shader.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum HdAnim {
Hack,
Walk,
Attack,
}
pub const ANIM_FRAMES: u32 = 4;
struct HdSprite {
kind: EK,
idle: &'static [u8],
idle_32: &'static [u8],
anims: &'static [(HdAnim, &'static [u8], &'static [u8])], // 64x64 and 32x32 sheets
}
macro_rules! hd_sprite {
($kind:expr, $name:literal) => {
HdSprite {
kind: $kind,
idle: include_bytes!(concat!("../assets/sprites/", $name, "-64.png")),
idle_32: include_bytes!(concat!("../assets/sprites/", $name, "-32.png")),
anims: &[],
}
};
}
const HD_SPRITES: [HdSprite; 31] = [
HdSprite {
kind: EK::Worker,
idle: include_bytes!("../assets/sprites/imp-64.png"),
idle_32: include_bytes!("../assets/sprites/imp-32.png"),
anims: &[(HdAnim::Hack, include_bytes!("../assets/sprites/imp-hack-64.png"), include_bytes!("../assets/sprites/imp-hack-32.png"))],
},
hd_sprite!(EK::Kobold, "kobold"),
hd_sprite!(EK::Spider, "spider"),
hd_sprite!(EK::Mage, "mage"),
hd_sprite!(EK::Ogre, "ogre"),
hd_sprite!(EK::Knight, "knight"),
hd_sprite!(EK::Archer, "archer"),
hd_sprite!(EK::Priest, "priest"),
hd_sprite!(EK::Dwarf, "dwarf"),
hd_sprite!(EK::Paladin, "paladin"),
hd_sprite!(EK::Jailer, "jailer"),
hd_sprite!(EK::Priestess, "priestess"),
hd_sprite!(EK::Chainbreaker, "chainbreaker"),
hd_sprite!(EK::Bat, "bat"),
hd_sprite!(EK::Gargoyle, "gargoyle"),
hd_sprite!(EK::Owl, "owl"),
hd_sprite!(EK::Wyrm, "wyrm"),
hd_sprite!(EK::Cogling, "cogling"),
hd_sprite!(EK::Revenant, "revenant"),
hd_sprite!(EK::Giant, "giant"),
hd_sprite!(EK::Fairy, "fairy"),
hd_sprite!(EK::BattleMage, "battlemage"),
hd_sprite!(EK::Ram, "ram"),
hd_sprite!(EK::Catapult, "catapult"),
hd_sprite!(EK::CaveBear, "cavebear"),
hd_sprite!(EK::Beetle, "beetle"),
hd_sprite!(EK::Trader, "trader"),
hd_sprite!(EK::Thief, "thief"),
hd_sprite!(EK::Monk, "monk"),
hd_sprite!(EK::Ranger, "ranger"),
hd_sprite!(EK::Inquisitor, "inquisitor"),
];
// a detailed sprite's cell: the same shape as a 10x10 sprite's 12x12 cell (6 texels per pixel),
// so the billboard is the same; the 64x64 picture sits where the 10x10 sprite would; the 32x32
// level has cells of half the size
pub const HD_CELL: u32 = SPR_CELL * 6;
const HD_COLS64: u32 = 56;
const HD_COLS32: u32 = 112;
// the pictures one after another: per sprite its frames (idle, then every animation's frames),
// per frame without and (kinds with a sack) with the sack, per those the five views
fn hd_frames(j: usize) -> u32 {
1 + HD_SPRITES[j].anims.len() as u32 * ANIM_FRAMES
}
fn hd_sacks(j: usize) -> u32 {
if SACK_KINDS.contains(&HD_SPRITES[j].kind) { 2 } else { 1 }
}
fn hd_base(j: usize) -> u32 {
(0..j).map(|i| hd_frames(i) * hd_sacks(i) * VIEWS).sum()
}
fn hd_pictures() -> u32 {
hd_base(HD_SPRITES.len())
}
// the size of a detailed sprite texture (fine: the 64x64 one)
pub fn hd_tex_size(fine: bool) -> (u32, u32) {
let (cols, cell) = if fine { (HD_COLS64, HD_CELL) } else { (HD_COLS32, HD_CELL / 2) };
(cols * cell, hd_pictures().div_ceil(cols) * cell)
}
// where a picture's gear goes: rows and columns of a 72x72 cell as shares of it (0..1)
#[derive(Clone, Copy, Debug, Default)]
pub struct GearAnchor {
pub top: f32, // top of the head
pub bottom: f32, // the feet
pub head: (f32, f32), // the head's left and right edge (a little below its top)
pub shoulder: f32, // the shoulders' row
pub sh: (f32, f32), // the shoulders' left and right edge
}
static HD_ANCHORS: std::sync::OnceLock<Vec<GearAnchor>> = std::sync::OnceLock::new();
fn gear_anchor(g: &Cell) -> GearAnchor {
let n = g.len();
let f = |v: usize| v as f32 / n as f32;
let span = |y: usize| -> Option<(usize, usize)> {
let xs: Vec<usize> = (0..n).filter(|&x| g[y][x].is_some()).collect();
Some((*xs.first()?, *xs.last()?))
};
let rows: Vec<usize> = (0..n).filter(|&y| span(y).is_some()).collect();
let (Some(&top), Some(&bot)) = (rows.first(), rows.last()) else { return GearAnchor::default() };
let h = bot - top + 1;
let head_y = (top + h / 10).min(bot);
let head = span(head_y).unwrap_or((n / 3, 2 * n / 3));
// the shoulders: the widest row of the upper body (tools and wings stick out: the middle
// row of the widest few counts)
let (a, b) = (top + h * 3 / 10, (top + h / 2).min(bot));
let shoulder = (a..=b).max_by_key(|&y| span(y).map_or(0, |(l, r)| r - l)).unwrap_or(a);
let sh = span(shoulder).unwrap_or(head);
GearAnchor { top: f(top), bottom: f(bot + 1), head: (f(head.0), f(head.1 + 1)), shoulder: f(shoulder), sh: (f(sh.0), f(sh.1 + 1)) }
}
// the gear's parts in their texture (grey, tinted by the metal or cloth colour at draw time):
// (x, y, w, h) in texels of GEAR_TEX
pub const GEAR_TEX: u32 = 64;
pub const GEAR_HELMET: (u32, u32, u32, u32) = (0, 0, 24, 12);
pub const GEAR_PAULDRON: (u32, u32, u32, u32) = (24, 0, 12, 9);
pub const GEAR_CROWN: (u32, u32, u32, u32) = (36, 0, 20, 10);
pub const GEAR_CAPE: (u32, u32, u32, u32) = (0, 14, 28, 40);
pub const GEAR_HORN: (u32, u32, u32, u32) = (28, 14, 7, 10);
pub const GEAR_PLUME: (u32, u32, u32, u32) = (36, 14, 7, 12);
// kinds that carry treasure in a sack on the back (instead of the coin sign over the head);
// their 10x10 sprite with the sack gets an own atlas column after SPRITE_KINDS
const SACK_KINDS: [EK; 1] = [EK::Worker];
// the sack (k outline, B burlap, b its shade and the rope, y/Y gold coins at the opening)
const SACK_SMALL: [&str; 5] = [".kk.", "kyYk", "kBbk", "kBBk", ".kk."];
const SACK_32: [&str; 12] = [
"....kkkk....",
"...kyYYyk...",
"..kyYyyYyk..",
"..kkbbbbkk..",
"...kBBBBk...",
"..kBBBBBBk..",
".kBBBbBBBBk.",
".kBBBBBBbBk.",
".kBbBBBBBBk.",
".kBBBBBBBBk.",
"..kBBBBBBk..",
"...kkkkkk...",
];
// the same, finer for the 64x64 sprites (d: the shadow side)
const SACK_64: [&str; 22] = [
".......kkkkkk.......",
".....kkyYYYyykk.....",
"....kyYYyYYyyYyk....",
"...kyYyyYyYYyyYyk...",
"...kkyYYyyyYYyykk...",
"....kkbbbbbbbbkk....",
".....kbBbbbbBbk.....",
"......kBBBBBBk......",
".....kBBBBBBBBk.....",
"....kBBBBBBBBBBk....",
"...kBBBBbBBBBBBBk...",
"..kBBBBBBBBBbBBBBk..",
"..kBBbBBBBBBBBBBBk..",
".kBBBBBBBBBBBBbBBBk.",
".kBBBBBBBbBBBBBBBBk.",
".kBBBBBBBBBBBBBBBdk.",
".kBBbBBBBBBBBBBBddk.",
".kBBBBBBBBBBBBBdddk.",
"..kBBBBBBBBBBBdddk..",
"..kdBBBBBBBBBddddk..",
"...kkddddddddddkk...",
".....kkkkkkkkkk.....",
];
fn sprite_rows(k: EK) -> [&'static str; 10] {
match k {
EK::Worker => [
"..........",
"k..kkkk..k",
"okkooookko",
".koeooeok.",
"..kooook..",
"...kRRk..S",
"..kRRRRkSS",
"..kRRRRkb.",
"...kRRkb..",
"...k..k...",
],
EK::Kobold => [
"........s.",
"..kkkk..s.",
".kggggk.b.",
".kgegegkb.",
".kggggkkb.",
"..kGGkggb.",
".kgGGGgkb.",
".kgGGGk.b.",
"..kGkGk...",
"..kk.kk...",
],
EK::Spider => [
"..........",
"...kkkk...",
"k.kPPPPk.k",
".kPPPPPPk.",
"kkPePPePkk",
".kPPPPPPk.",
"k.kPPPPk.k",
".k.kkkk.k.",
"k........k",
"..........",
],
EK::Mage => [
"....kk....",
"...kPPk...",
"..kPPPPk..",
".kkkkkkkk.",
"..kfefek.y",
"..kPPPPk.b",
".kPpPPpPkb",
".kPPpPPPkb",
".kPPPPPPk.",
"..kkkkkk..",
],
EK::Ogre => [
"..kkkkkk..",
".kcccccck.",
".kcecceck.",
".kcCccCck.",
"kcccccccck",
"kcCcccccCk",
"kccCccCcck",
".kcccccck.",
".kcCk.kCk.",
".kkk...kk.",
],
EK::Knight => [
"...kkkk...",
"..kssssk..",
"..kSkkSk.s",
"..kssssk.s",
"..kuuuuk.s",
".ksuuuusks",
".kSuyyuSk.",
"..kuuuuk..",
"..kSkkSk..",
"..kk..kk..",
],
EK::Archer => [
"...kkkk..b",
"..kGGGGkbk",
"..kGfkfkb.",
"..kGfffkb.",
"..kgggggb.",
".kfgggggb.",
"..kggggkbk",
"..kBBBBk.b",
"..kBkkBk..",
"..kk..kk..",
],
// thief: a dark hood and mask, a dagger, a sack of gold on the back
EK::Thief => [
"...kkkk...",
"..kddddk..",
"..kdfkfk.Y",
"..kddddkYy",
".kdddddkYy",
"skdddddk..",
".kdkdddk..",
"..kddddk..",
"..kdkkdk..",
"..kk..kk..",
],
// monk: shaven head, an orange robe tied with a rope, bare fists
EK::Monk => [
"...kkkk...",
"..kffffk..",
"..kfkfkk..",
"..kffffk..",
".kooooook.",
"kfoooooofk",
".kooyyook.",
".kOooooOk.",
".kOooooOk.",
"..kkkkkk..",
],
// ranger: a green hood and cloak, a long bow, leather boots
EK::Ranger => [
"...kkkk.b.",
"..kgGGgkkb",
"..kGfkfk.b",
"..kGfffk.b",
".kGgggggkb",
".kGgbbggkb",
".kGggggk.b",
"..kBBBBk.b",
"..kBkkBkb.",
"..kk..kk..",
],
// inquisitor: a tall red hat, white and red robes, a glowing holy book
EK::Inquisitor => [
"...kRRk...",
"..kRrrRk..",
"..kRrrRk..",
"..kffffk..",
"..kfkfkk..",
".kwwrrwwk.",
"kwkwrrwkek",
".kwwrrwwkk",
".kRwwwwRk.",
"..kkkkkk..",
],
EK::Priest => [
"....yy....",
"...kkkk...",
"..kffffk..",
"..kfkfkk..",
"..kwwwwk..",
".kwwyywwk.",
".kwwyywwk.",
".kwwwwwwk.",
".kwwwwwwk.",
"..kkkkkk..",
],
EK::Dwarf => [
"..........",
"...kkkk...",
"..kSSSSk..",
"..kSSSSkS.",
"..kfkfkkSS",
"..kooook.b",
".kboooobkb",
".kbBBBBbk.",
"..kBkkBk..",
"..kk..kk..",
],
EK::Jailer => [
"...kkkk...",
"..kCCCCk..",
"..kCkkCk..",
"..kCeeCk..",
".kCCCCCCk.",
"kCCRRRRCCk",
"kCCRRRRCCS",
".kCCCCCCkS",
"..kCk.kCkS",
"..kk...kk.",
],
EK::Priestess => [
"....oo..y.",
"...kkkk.o.",
"..kffffk..",
"..kfkfkk..",
"..kRRRRk..",
".kRRooRRk.",
".kRRooRRkf",
".kRRRRRRk.",
".kRRRRRRk.",
"..kkkkkk..",
],
// flying creatures
EK::Bat => [
"..........",
"k..k..k..k",
"Pk.kPPk.kP",
"PPkPPPPkPP",
"PPPPrPrPPP",
"PPPPPPPPPP",
".PPkPPkPP.",
"..k.PP.k..",
".....k....",
"..........",
],
EK::Gargoyle => [
"w........w",
"Cw.kkkk.wC",
"CCkceeckCC",
"CCkcccckCC",
"CkcccccckC",
"k.kcCCck.k",
".kcccccck.",
".kcCkkCck.",
".kck..kck.",
".kk....kk.",
],
EK::Owl => [
"..k....k..",
".kBk..kBk.",
".kbbbbbbk.",
"kbeebbeebk",
"kbekbbekbk",
".kbbyybbk.",
"kubbbbbbuk",
"kubbwwbbuk",
".kubbbbuk.",
"..ky..yk..",
],
// cogling: a sooty little tinkerer with brass goggles and a hammer
EK::Cogling => [
"........SS",
"...kkkk.SS",
"..kOOOOk.b",
".kyekkeykb",
"..kOooOk.b",
"...kCCk..b",
"..kOCCOkbb",
"..kOCCOk..",
"...kO.Ok..",
"...kk.kk..",
],
// revenant: a pale, hollow-eyed corpse with a rusty blade and green grave-light
EK::Revenant => [
"...kkkk...",
"..kwwwwk..",
"..kwgwgk..",
"..kwwwwk.S",
"...kCCk..S",
"..kCcCCk.S",
".kCkcCkCkb",
"..kCcCCk..",
"..kCk.kCk.",
"..kk...kk.",
],
// giant: a huge brute in a leather jerkin with a tree-trunk club
EK::Giant => [
"...kkkk...",
"..kffffk..",
"..kfefek.b",
"..kffffk.b",
".kBBBBBBkb",
"kfBuuuuBfb",
"kfBuuuuBf.",
".kBBBBBBk.",
".kBBk.kBBk",
".kkk...kkk",
],
// fairy: a tiny glowing spy with dragonfly wings
EK::Fairy => [
"..........",
"ww.....ww.",
"www.kk.www",
".wwkffkww.",
"..wkefkw..",
"...kuuk...",
"...kuuk...",
"....kk....",
"...y..y...",
"..........",
],
// trader: a hooded merchant with a big pack and a lantern
EK::Trader => [
"...kkk....",
"..kPPPk.BB",
".kPfefPkBb",
".kPffffkBb",
"..kPPPPkBB",
".kPPyPPPk.",
"ykPPPPPPk.",
"o.kPPPPk..",
"..kPk.kPk.",
"..kk...kk.",
],
// cave bear: shaggy brown fur, small yellow eyes
EK::CaveBear => [
".kk....kk.",
"kBBk..kBBk",
"kBBBBBBBBk",
"kBeBBBBeBk",
"kBBBkkBBBk",
".kBBffBBk.",
"kBBBBBBBBk",
"kBbBBBBbBk",
".kBBkkBBk.",
".kkk..kkk.",
],
// giant beetle: a shiny dark-green shell, mandibles, six legs
EK::Beetle => [
"..k....k..",
"...k..k...",
"..kGGGGk..",
".kGeGGeGk.",
"kGGgGGgGGk",
"kGgGGGGgGk",
"kGGgGGgGGk",
".kGGGGGGk.",
"k.kGGGGk.k",
".k.k..k.k.",
],
// battering ram: a roofed frame on wheels, an iron-capped trunk
EK::Ram => [
"..........",
"...BBBB...",
"..BbbbbB..",
".BbbbbbbB.",
"SsbbbbbbB.",
"SsBBBBBBB.",
"..b.b.b.b.",
".kBk..kBk.",
".kck..kck.",
"..k....k..",
],
// catapult: a throwing arm with a stone in its cup
EK::Catapult => [
"......cc..",
".....kCCk.",
".....kbk..",
"....kb....",
"...kb.....",
"..kb......",
".BBBBBBBB.",
".bkbbbbkb.",
".kck..kck.",
"..k....k..",
],
// battle mage: robes of the order, a burning staff
EK::BattleMage => [
"....kk...o",
"...kuuk.oy",
"..kuuuuk.o",
".kkffffk.b",
"..kfefek.b",
"..kwuuwk.b",
".kuwuuwukb",
".kuuwwuuk.",
".kuuuuuuk.",
"..kkkkkk..",
],
EK::Wyrm => [
"R.......kw",
"RR....krrk",
"RRR..krekr",
"RRRRkrrrrk",
".RRrrrrok.",
"..rrooork.",
".krrooorr.",
"krrrrrrRk.",
"k.rrk.rk..",
"..kk..kk..",
],
// obsidian demon with glowing cracks, broken chains and twin axes
EK::Chainbreaker => [
"s.kkkkkk.s",
"sskdoodkss",
".SkeddekS.",
".SkdoodkS.",
"kdddoodddk",
"kdoddddodk",
"SkddooddkS",
".kddddddk.",
".kdok.odk.",
".kkk..kkk.",
],
EK::Paladin => [
"...kkkk..w",
"..kyyyyk.w",
"..kYkkYk.w",
"..kyyyykkw",
".kwwwwwwkk",
"kywwyywwyk",
".kwyyyywk.",
"..kwwwwk..",
"..kYkkYk..",
"..kk..kk..",
],
}
}
fn pal(ch: char) -> Option<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
}
// a picture into a texture of `cell` x `cell` cells, `cols` per row (no white silhouette:
// the shader flashes the detailed sprites)
fn put_picture(img: &mut Image, pic: u32, cols: u32, cell: u32, g: &Cell) {
let (ox, oy) = ((pic % cols) * cell, (pic / cols) * cell);
for (y, line) in g.iter().enumerate() {
for (x, p) in line.iter().enumerate() {
if let Some(c) = p {
img.set_pixel(ox + x as u32, oy + y as u32, *c);
}
}
}
}
// the two textures of the detailed sprites (64x64 level, 32x32 level); the gear anchors of every
// picture are measured on the way
fn gen_hd_sprites() -> (Image, Image) {
let (w64, h64) = hd_tex_size(true);
let (w32, h32) = hd_tex_size(false);
let clear = Color::new(0.0, 0.0, 0.0, 0.0);
let mut fine = Image::gen_image_color(w64 as u16, h64 as u16, clear);
let mut coarse = Image::gen_image_color(w32 as u16, h32 as u16, clear);
let mut anchors = vec![GearAnchor::default(); hd_pictures() as usize];
let png = |b: &[u8]| Image::from_file_with_format(b, Some(ImageFormat::Png)).expect("hd sprite");
for (j, hd) in HD_SPRITES.iter().enumerate() {
let sheets64: Vec<Image> = std::iter::once(png(hd.idle)).chain(hd.anims.iter().map(|a| png(a.1))).collect();
let sheets32: Vec<Image> = std::iter::once(png(hd.idle_32)).chain(hd.anims.iter().map(|a| png(a.2))).collect();
let sacks = hd_sacks(j);
for frame in 0..hd_frames(j) {
// the frame's sheet and its place there: idle (frame 0) or frame k of an animation
let (sheet, fx) = if frame == 0 { (0, None) } else { (1 + ((frame - 1) / ANIM_FRAMES) as usize, Some((frame - 1) % ANIM_FRAMES)) };
for view in 0..VIEWS {
let at = |n: u32| match fx {
None => (view * n, 0),
Some(f) => (f * n, view * n),
};
let (x64, y64) = at(64);
let (x32, y32) = at(32);
let g64 = hd_cell(&sheets64[sheet], x64, y64, 64, HD_CELL);
let g32 = hd_cell(&sheets32[sheet], x32, y32, 32, HD_CELL / 2);
for sack in 0..sacks {
let pic = hd_base(j) + (frame * sacks + sack) * VIEWS + view;
let (mut a, mut b) = (g64.clone(), g32.clone());
if sack == 1 {
add_sack(&mut a, view, &SACK_64, (0.7, -0.65));
add_sack(&mut b, view, &SACK_32, (0.7, -0.65));
}
anchors[pic as usize] = gear_anchor(&g64);
put_picture(&mut fine, pic, HD_COLS64, HD_CELL, &a);
put_picture(&mut coarse, pic, HD_COLS32, HD_CELL / 2, &b);
}
}
}
}
let _ = HD_ANCHORS.set(anchors);
(fine, coarse)
}
// the gear parts (see GEAR_HELMET etc.): grey with dark outlines, tinted at draw time
fn gen_gear() -> Image {
let mut img = Image::gen_image_color(GEAR_TEX as u16, GEAR_TEX as u16, Color::new(0.0, 0.0, 0.0, 0.0));
let grey = |v: f32| Color::new(v, v, v, 1.0);
let outline = Color::new(0.08, 0.07, 0.09, 1.0);
// a shape given by an inside test, outlined, lit from the upper left
let mut shape = |(ox, oy, w, h): (u32, u32, u32, u32), inside: &dyn Fn(f32, f32) -> bool, shade: &dyn Fn(f32, f32) -> f32| {
for y in 0..h {
for x in 0..w {
let (fx, fy) = ((x as f32 + 0.5) / w as f32, (y as f32 + 0.5) / h as f32);
if !inside(fx, fy) {
continue;
}
let edge = [(-1i32, 0i32), (1, 0), (0, -1), (0, 1)].iter().any(|&(dx, dy)| {
let (nx, ny) = (x as i32 + dx, y as i32 + dy);
nx < 0 || ny < 0 || nx >= w as i32 || ny >= h as i32 || !inside((nx as f32 + 0.5) / w as f32, (ny as f32 + 0.5) / h as f32)
});
img.set_pixel(ox + x, oy + y, if edge { outline } else { grey(shade(fx, fy)) });
}
}
};
// helmet: a dome with a rim
shape(GEAR_HELMET, &|x, y| (x - 0.5).powi(2) * 4.0 + (1.0 - y).powi(2) <= 1.0, &|x, y| if y > 0.75 { 0.55 } else { 0.95 - (x * 0.35 + y * 0.25) });
// shoulder piece: a rounded plate with a rivet
shape(GEAR_PAULDRON, &|x, y| (x - 0.5).powi(2) * 4.0 + (y - 0.6).powi(2) * 2.6 <= 1.0, &|x, y| if (x - 0.5).abs() < 0.1 && (y - 0.5).abs() < 0.12 { 1.0 } else { 0.9 - y * 0.4 });
// crown: a band with three spikes
shape(GEAR_CROWN, &|x, y| y > 0.55 || ((x * 3.0).fract() - 0.5).abs() < y * 0.55, &|x, y| if y > 0.6 && (x * 5.0).fract() < 0.25 { 0.6 } else { 1.0 - x * 0.2 });
// cape: wider at the hem, with folds
shape(GEAR_CAPE, &|x, y| (x - 0.5).abs() < 0.32 + 0.18 * y, &|x, y| 0.8 - (((x - 0.5) * 9.0 + y).sin().abs()) * 0.25 - y * 0.15);
// horn: a curved spike (bone)
shape(GEAR_HORN, &|x, y| (x - (1.0 - y) * 0.4 - 0.3).abs() < 0.16 + 0.3 * y, &|x, y| 0.95 - y * 0.25 - x * 0.1);
// plume: a feather bush
shape(GEAR_PLUME, &|x, y| (x - 0.5).powi(2) * 3.2 + (y - 0.5).powi(2) * 1.1 <= 0.26 + 0.04 * (y * 14.0).sin(), &|_x, y| 0.95 - y * 0.3);
img
}
// ------------------------------------------------------------------ mouse pointers (16x16)
pub const CURSOR_POINT: u32 = 0; // pointing monster hand (hot spot: finger tip)
pub const CURSOR_OPEN: u32 = 1; // open grabbing hand (hot spot: palm centre)
pub const CURSOR_FIST: u32 = 2; // holding something
pub const CURSOR_BACKHAND: u32 = 3; // slap, with motion streaks
pub const CURSOR_PUNCH: u32 = 4; // punch, with impact star
pub const CURSOR_HOT: [(f32, f32); 5] = [(2.0, 1.0), (7.0, 7.0), (7.0, 7.0), (7.0, 7.0), (7.0, 7.0)];
const CUR_POINT: [&str; 16] = [
"..kk............",
".kwwk...........",
".kggk...........",
".kggk...........",
".kggkkk.........",
".kggkggkkk......",
".kggkggkggkk....",
"kkggggggggggk...",
"kgkgggggggggk...",
"kggggggggggGk...",
".kgggggggggGk...",
"..kggggggggGk...",
"..kgggggggGk....",
"...kGGGGGGk.....",
"...kkkkkkkk.....",
"................",
];
// the hand tool: an open goblin hand (four fingers, the thumb at the side), lit fingertips
const CUR_OPEN: [&str; 16] = [
"...kk.kk.kk.....",
"..klkklkklk.kk..",
"..kgkkgkkgk.klk.",
"..kgkkgkkgkkgk..",
"..kgkkgkkgkkgk..",
"..kggggggggggk..",
"kk.kgggggggggk..",
"klkkggggggggGk..",
"kggkggggggggGk..",
".kgggggggggGGk..",
"..kggggggggGk...",
"...kgggggggGk...",
"....kggggGGk....",
".....kkkkkk.....",
"................",
"................",
];
// holding units: a fist, knuckles on top, the thumb across
const CUR_FIST: [&str; 16] = [
"................",
"................",
"....kkkkkkk.....",
"...klklklklk....",
"..kgkgkgkgGk....",
"..kgkgkgkgGGk...",
".kkkkkkkggGGk...",
"kllllllkgGGk....",
"kggggggkgGGk....",
".kkkkkkggGGk....",
"..kgggggggGk....",
"...kggggggGk....",
"....kGGGGGk.....",
".....kkkkk......",
"................",
"................",
];
fn cursor_pal(ch: char) -> Option<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 (hd64, hd32) = gen_hd_sprites();
let hd_sprites = Texture2D::from_image(&hd64);
hd_sprites.set_filter(FilterMode::Nearest);
let hd_sprites32 = Texture2D::from_image(&hd32);
hd_sprites32.set_filter(FilterMode::Nearest);
let gear = Texture2D::from_image(&gen_gear());
gear.set_filter(FilterMode::Nearest);
let mat = |b: &[u8]| {
let t = Texture2D::from_file_with_format(b, Some(ImageFormat::Png));
t.set_filter(FilterMode::Nearest);
t
};
let materials = MATERIALS.map(|(hd, _)| mat(hd));
let materials_px = MATERIALS.map(|(_, px)| mat(px));
Gfx { atlas, sprites, hd_sprites, hd_sprites32, gear, atlas_hd, atlas_hd2, materials, materials_px, cursors, badges, font, font_map }
}
pub fn cell(id: u32) -> Rect {
Rect::new((id % ATLAS_COLS) as f32 * CELL, (id / ATLAS_COLS) as f32 * CELL, CELL, CELL)
}
// the plain 10x10 sprite (interface icons)
pub fn sprite_rect(k: EK, white: bool) -> Rect {
let i = sprite_index(k);
let k = SPR_K as f32;
Rect::new((i * ACELL) as f32 + k, (if white { ACELL } else { 0 }) as f32 + k, SPR as f32 * k, SPR as f32 * k)
}
// the whole 12x12 cell with the gear of a level (billboards)
pub fn sprite_cell(k: EK, white: bool, level: u32) -> Rect {
Gfx::sprite_cell_view(k, white, level, 0)
}
// the cell of one of the five views (see VIEWS)
pub fn sprite_cell_view(k: EK, white: bool, level: u32, view: u32) -> Rect {
let i = sprite_index(k);
let row = (view.min(VIEWS - 1) * GEAR_TIERS + gear_tier(level)) * 2 + white as u32;
Rect::new((i * ACELL) as f32, (row * ACELL) as f32, ACELL as f32, ACELL as f32)
}
// the cell of a detailed sprite's view, if the kind has one (same layout as sprite_cell_view)
// the picture of a detailed sprite, if the kind has one: a view, standing or a frame of an
// animation (an animation the kind does not have: standing), with or without the sack
pub fn hd_picture(k: EK, view: u32, anim: Option<(HdAnim, u32)>, sack: bool) -> Option<u32> {
let j = HD_SPRITES.iter().position(|h| h.kind == k)?;
let frame = anim
.and_then(|(a, f)| HD_SPRITES[j].anims.iter().position(|x| x.0 == a).map(|i| 1 + i as u32 * ANIM_FRAMES + f % ANIM_FRAMES))
.unwrap_or(0);
let sacks = hd_sacks(j);
let sack = if sacks > 1 { sack as u32 } else { 0 };
Some(hd_base(j) + (frame * sacks + sack) * VIEWS + view.min(VIEWS - 1))
}
// whether a kind has a drawn animation
pub fn hd_has_anim(k: EK, anim: HdAnim) -> bool {
HD_SPRITES.iter().any(|h| h.kind == k && h.anims.iter().any(|a| a.0 == anim))
}
// a picture's place in its texture as texture coordinates (fine: hd_sprites, else hd_sprites32)
pub fn hd_uv(pic: u32, fine: bool) -> Rect {
let (cols, cell) = if fine { (HD_COLS64, HD_CELL) } else { (HD_COLS32, HD_CELL / 2) };
let (w, h) = hd_tex_size(fine);
let (x, y) = ((pic % cols) * cell, (pic / cols) * cell);
Rect::new(x as f32 / w as f32, y as f32 / h as f32, cell as f32 / w as f32, cell as f32 / h as f32)
}
// where the gear of a picture goes (measured when the textures were made)
pub fn hd_anchor(pic: u32) -> GearAnchor {
HD_ANCHORS.get().and_then(|a| a.get(pic as usize).copied()).unwrap_or_default()
}
// whether a kind carries its treasure in a sack on the back
pub fn has_sack(k: EK) -> bool {
SACK_KINDS.contains(&k)
}
// the cell of a 10x10 sprite with the sack on the back (only SACK_KINDS)
pub fn sprite_cell_sack(k: EK, white: bool, level: u32, view: u32) -> Option<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
}
}
}
}
// an atlas cell in the interface (tool and room buttons); the high-res style shows the
// high-res atlas, so the room buttons show the rooms' new floors
pub fn draw_cell(&self, id: u32, x: f32, y: f32, w: f32, h: f32, color: Color) {
let (tex, k) = if HIGH_RES_UI.with(|h| h.get()) { (&self.atlas_hd, (HD_TILE / 16) as f32) } else { (&self.atlas, 1.0) };
let r = Self::cell(id);
draw_texture_ex(
tex,
x,
y,
color,
DrawTextureParams { dest_size: Some(vec2(w, h)), source: Some(Rect::new(r.x * k, r.y * k, r.w * k, r.h * k)), ..Default::default() },
);
}
// draw a single character (x, y = top-left corner)
pub fn glyph(&self, ch: char, x: f32, y: f32, scale: f32, color: Color) {
let (sx, sy) = font_src(&self.font_map, ch);
let src = Rect::new(sx, sy, GW, GH);
draw_texture_ex(
&self.font,
x.floor(),
y.floor(),
color,
DrawTextureParams { dest_size: Some(vec2(GW * scale, GH * scale)), source: Some(src), ..Default::default() },
);
}
// draw text (x, y = top-left corner); scale should be an integer
pub fn text(&self, s: &str, x: f32, y: f32, scale: f32, color: Color) {
let mut cx = x;
for ch in s.chars() {
if ch != ' ' {
self.glyph(ch, cx, y, scale, color);
}
cx += GW * scale;
}
}
pub fn measure(&self, s: &str, scale: f32) -> f32 {
s.chars().count() as f32 * GW * scale
}
// Military rank badge for a level (k = size of one badge pixel in device pixels):
// Lv 2-4: 1-3 bronze chevrons, Lv 5-7: 1-3 silver bars, Lv 8-9: 1-2 gold stars,
// Lv 10: a gold star in a green wreath. Lv 1 has none. Returns the width drawn.
pub fn badge(&self, x: f32, y: f32, level: u32, k: f32) -> f32 {
let px = |gx: i32, gy: i32, c: Color| draw_rectangle(x + gx as f32 * k, y + gy as f32 * k, k, k, c);
let plate = |w: i32, h: i32| draw_rectangle(x - k, y - k, (w + 2) as f32 * k, (h + 2) as f32 * k, Color::new(0.05, 0.03, 0.04, 0.85));
let bronze = Color::from_rgba(214, 140, 70, 255);
let silver = Color::from_rgba(215, 222, 235, 255);
let gold = Color::from_rgba(255, 210, 70, 255);
let star = |ox: i32, oy: i32, c: Color| {
for (gx, gy) in [(2, 0), (1, 1), (2, 1), (3, 1), (0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (1, 3), (2, 3), (3, 3), (0, 4), (1, 4), (3, 4), (4, 4)] {
px(ox + gx, oy + gy, c);
}
};
match level {
0 | 1 => 0.0,
2..=4 => {
let n = (level - 1) as i32;
plate(5, n * 2 + 1);
for c in 0..n {
// a chevron pointing up
for (gx, gy) in [(2, 0), (1, 1), (3, 1), (0, 2), (4, 2)] {
px(gx, c * 2 + gy - if gy == 2 { 1 } else { 0 }, bronze);
}
}
7.0 * k
}
5..=7 => {
let n = (level - 4) as i32;
plate(5, n * 2 - 1);
for b in 0..n {
for gx in 0..5 {
px(gx, b * 2, silver);
}
}
7.0 * k
}
8 | 9 => {
let n = (level - 7) as i32;
plate(n * 6 - 1, 5);
for s in 0..n {
star(s * 6, 0, gold);
}
(n * 6 + 1) as f32 * k
}
_ => {
plate(9, 7);
let green = Color::from_rgba(90, 170, 80, 255);
for (gx, gy) in [(0, 2), (0, 3), (0, 4), (1, 5), (2, 6), (8, 2), (8, 3), (8, 4), (7, 5), (6, 6), (3, 6), (5, 6), (1, 1), (7, 1)] {
px(gx, gy, green);
}
star(2, 1, gold);
11.0 * k
}
}
}
// text with a dark shadow
pub fn text_sh(&self, s: &str, x: f32, y: f32, scale: f32, color: Color) {
self.text(s, x + scale, y + scale, scale, Color::new(0.0, 0.0, 0.0, color.a * 0.85));
self.text(s, x, y, scale, color);
}
// window with a box-drawing frame; size in characters (cols x rows incl. frame)
pub fn window(&self, x: f32, y: f32, cols: i32, rows: i32, scale: f32, double: bool, frame: Color, bg: Color) {
let cw = GW * scale;
let ch = GH * scale;
draw_rectangle(x, y, cols as f32 * cw, rows as f32 * ch, bg);
let b = if double { BOX_DOUBLE } else { BOX_SINGLE };
for cx in 0..cols {
for cy in 0..rows {
let top = cy == 0;
let bottom = cy == rows - 1;
let left = cx == 0;
let right = cx == cols - 1;
let code = match (top, bottom, left, right) {
(true, _, true, _) => b[0],
(true, _, _, true) => b[1],
(_, true, true, _) => b[2],
(_, true, _, true) => b[3],
(true, _, _, _) | (_, true, _, _) => b[4],
(_, _, true, _) | (_, _, _, true) => b[5],
_ => continue,
};
self.glyph(code, x + cx as f32 * cw, y + cy as f32 * ch, scale, frame);
}
}
}
// window with a title in the top frame line
pub fn window_titled(&self, x: f32, y: f32, cols: i32, rows: i32, scale: f32, double: bool, frame: Color, bg: Color, title: &str, tcol: Color) {
self.window(x, y, cols, rows, scale, double, frame, bg);
if !title.is_empty() {
let cw = GW * scale;
let t = format!(" {} ", title);
let n = t.chars().count() as i32;
// the frame line ends cleanly on both sides of the title (rope ends, or the double
// strand's caps), if there is room for them
let caps = n + 5 <= cols;
let tx = x + if caps { 1.0 } else { 2.0 } * cw;
let w = cw * (n + if caps { 2 } else { 0 }) as f32;
draw_rectangle(tx, y, w, GH * scale, bg);
if caps {
let (l, r) = if double { ('╡', '╞') } else { ('╴', '╶') };
self.glyph(l, tx, y, scale, frame);
self.text(&t, tx + cw, y, scale, tcol);
self.glyph(r, tx + cw * (n + 1) as f32, y, scale, frame);
} else {
self.text(&t, tx, y, scale, tcol);
}
}
}
}
#[cfg(test)]
mod hd_atlas_tests {
use super::*;
#[test]
fn high_res_atlas() {
let small = gen_atlas();
for tile in [HD_TILE, HD_TILE2] {
let hd = gen_hd_atlas(tile);
let k = tile / 16;
assert_eq!((hd.width as u32, hd.height as u32), (ATLAS_COLS * tile, ATLAS_ROWS * tile));
let px = |img: &Image, x: u32, y: u32| {
let p = img.get_pixel(x, y);
(p.r, p.g, p.b, p.a)
};
// the colours are matched to the 16x16 cells (see match_colors), but a picture keeps its
// shapes: what is lighter in the source stays lighter in the atlas
let placed = |hd: &Image, ox: u32, oy: u32, src: &Image, sx: u32, sy: u32| {
let lum = |p: Color| p.r + p.g + p.b;
let pts = [(3, 5), (7, 40), (30, 9), (50, 50), (20, 60), (61, 2)];
for &(ax, ay) in pts.iter() {
for &(bx, by) in pts.iter() {
let (s1, s2) = (lum(src.get_pixel(sx + ax, sy + ay)), lum(src.get_pixel(sx + bx, sy + by)));
if s1 + 0.15 < s2 {
assert!(lum(hd.get_pixel(ox + ax, oy + ay)) <= lum(hd.get_pixel(ox + bx, oy + by)) + 0.02, "cell at {ox},{oy}");
}
}
}
};
// a cell without an own texture: the 16x16 one scaled up
let (cx, cy) = ((T_PUMPKIN % ATLAS_COLS) * 16, (T_PUMPKIN / ATLAS_COLS) * 16);
for (x, y) in [(0, 0), (5, 9), (15, 15)] {
assert_eq!(px(&hd, (cx + x) * k + 2, (cy + y) * k + 1), px(&small, cx + x, cy + y));
}
// the generated ones: the quarters of their 2x2 sheet
for &(base, n, png, png2) in HD_TILES.iter() {
let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).unwrap();
let side = if n == 4 { 2 * tile } else { tile };
assert_eq!((src.width as u32, src.height as u32), (side, side));
for v in 0..n {
let id = base + v;
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
let (sx, sy) = if n == 4 { ((v % 2) * tile, (v / 2) * tile) } else { (0, 0) };
placed(&hd, ox, oy, &src, sx, sy);
}
}
// the sets: five tiles each in their own cells (inside the atlas)
for &(_, _, own, png, png2, _) in HD_SETS.iter() {
let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).unwrap();
assert_eq!((src.width as u32, src.height as u32), (5 * tile, tile));
assert!(own + 5 <= ATLAS_COLS * ATLAS_ROWS);
for v in 0..5 {
let id = own + v;
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
placed(&hd, ox, oy, &src, v * tile, 0);
}
}
}
// the icons: a strip with one icon per cell
for &(png, png2, cells) in HD_ICONS.iter() {
for tile in [HD_TILE, HD_TILE2] {
let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).unwrap();
assert_eq!((src.width as u32, src.height as u32), (cells.len() as u32 * tile, tile));
}
}
// the variants: as many tiles as the kind has cells, every cell filled
for &(base, n, png, png2) in HD_VARS.iter() {
for tile in [HD_TILE, HD_TILE2] {
let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).unwrap();
assert_eq!((src.width as u32, src.height as u32), (n * tile, tile), "cell {base}");
}
for z in 0..8 {
for x in 0..8 {
assert!((base..base + n).contains(&hd_tile(base, x, z)));
}
}
}
// the decals: transparent around their pictures
for &(id, _, _) in HD_DECALS.iter() {
for tile in [HD_TILE, HD_TILE2] {
let hd = gen_hd_atlas(tile);
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
let clear = (0..tile * tile).filter(|i| hd.get_pixel(ox + i % tile, oy + i / tile).a < 0.5).count() as u32;
assert!(clear > tile * tile / 3 && clear < tile * tile, "decal {id} at {tile}: {clear} clear texels");
}
}
// every tile of a set shows up
let mut n = [0u32; 5];
for z in 0..60 {
for x in 0..60 {
let id = hd_tile(T_CLAIM + (x as u32 % 4), x, z);
assert!((T_HDSET..T_HDSET + 5).contains(&id));
n[(id - T_HDSET) as usize] += 1;
}
}
assert!(n.iter().all(|&k| k > 100), "{n:?}");
assert!((T_HDSET + 15..T_HDSET + 20).contains(&hd_tile(T_RIVAL3 + 2, 3, 7)));
assert!((T_HDSET + 50..T_HDSET + 55).contains(&hd_tile(T_DIAMOND + 1, 8, 2)));
assert!((T_HDSET + 25..T_HDSET + 30).contains(&hd_tile(T_EARTH + 3, 5, 5)));
// the liquids: four variants by position; others stay
assert_eq!(hd_tile(T_WATER + 3, 0, 0), T_WATER);
assert_eq!(hd_tile(T_LAVA, 1, 1), T_LAVA + 3);
assert_eq!(hd_tile(T_PUMPKIN + 2, 1, 0), T_PUMPKIN + 2);
}
}
#[cfg(test)]
mod atlas_dump {
use super::*;
// `cargo test --bins dump_atlas_cell -- --ignored` writes a 16x16 cell kind (env DUMP_CELL,
// default T_REINF: its four variants at random, 6x4 tiles) scaled up 8x to /tmp/atlas-cell.png
#[test]
#[ignore]
fn dump_atlas_cell() {
let base: u32 = std::env::var("DUMP_CELL").ok().and_then(|v| v.parse().ok()).unwrap_or(T_REINF);
let atlas = gen_atlas();
let (w, h) = (6 * 16 * 8, 4 * 16 * 8);
let mut out = Image::gen_image_color(w as u16, h as u16, BLACK);
for t in 0..24u32 {
let id = base + hash(t as i32, 3, 5) % 4;
let (ox, oy) = ((id % ATLAS_COLS) * 16, (id / ATLAS_COLS) * 16);
for y in 0..128 {
for x in 0..128 {
out.set_pixel((t % 6) * 128 + x, h - 1 - ((t / 6) * 128 + y), atlas.get_pixel(ox + x / 8, oy + y / 8));
}
}
}
out.export_png("/tmp/atlas-cell.png");
}
// `cargo test --bins dump_atlases -- --ignored`: the 16x16 atlas scaled up and the 64x64
// high-res atlas side by side in /tmp/atlases.png (to compare the colours of the styles)
#[test]
#[ignore]
fn dump_atlases() {
let small = gen_atlas();
let hd = gen_hd_atlas(HD_TILE);
let (w, h) = (hd.width as u32, hd.height as u32);
let mut out = Image::gen_image_color((2 * w + 16) as u16, h as u16, BLACK);
for y in 0..h {
for x in 0..w {
out.set_pixel(x, h - 1 - y, small.get_pixel(x / 4, y / 4));
out.set_pixel(w + 16 + x, h - 1 - y, hd.get_pixel(x, y));
}
}
out.export_png("/tmp/atlases.png");
}
}
#[cfg(test)]
mod sprite_tests {
use super::*;
// texels that differ between two cells
fn differ(img: &Image, a: Rect, b: Rect) -> usize {
let n = a.w as u32;
(0..n * n)
.filter(|i| {
let p = img.get_pixel(a.x as u32 + i % n, a.y as u32 + i / n);
let q = img.get_pixel(b.x as u32 + i % n, b.y as u32 + i / n);
(p.r, p.g, p.b, p.a) != (q.r, q.g, q.b, q.a)
})
.count()
}
// texels of a picture that differ with and without something (pictures of the 64x64 level)
fn pic_texels(img: &Image, pic: u32) -> Vec<(u8, u8, u8, u8)> {
let (ox, oy) = ((pic % HD_COLS64) * HD_CELL, (pic / HD_COLS64) * HD_CELL);
(0..HD_CELL * HD_CELL)
.map(|i| {
let p = img.get_pixel(ox + i % HD_CELL, oy + i / HD_CELL);
((p.r * 255.0) as u8, (p.g * 255.0) as u8, (p.b * 255.0) as u8, (p.a * 255.0) as u8)
})
.collect()
}
#[test]
fn detailed_sprites() {
let (fine, coarse) = gen_hd_sprites();
assert_eq!((fine.width as u32, fine.height as u32), hd_tex_size(true));
assert_eq!((coarse.width as u32, coarse.height as u32), hd_tex_size(false));
assert!(fine.width <= 4096 && fine.height <= 4096, "the texture fits every GPU");
// the imp: idle and the digging frames, with and without the sack
for view in 0..VIEWS {
for anim in [None, Some((HdAnim::Hack, 0)), Some((HdAnim::Hack, 3))] {
let p = Gfx::hd_picture(EK::Worker, view, anim, false).unwrap();
let s = Gfx::hd_picture(EK::Worker, view, anim, true).unwrap();
let (a, b) = (pic_texels(&fine, p), pic_texels(&fine, s));
assert!(a.iter().filter(|t| t.3 > 128).count() > 600, "view {view} {anim:?} drawn");
assert!(a.iter().zip(b.iter()).filter(|(x, y)| x != y).count() > 60, "view {view} {anim:?}: the sack shows");
}
}
assert!(Gfx::hd_has_anim(EK::Worker, HdAnim::Hack));
// every kind has its pictures; an animation it lacks shows it standing
let mut seen = std::collections::HashSet::new();
for &k in SPRITE_KINDS.iter() {
for view in 0..VIEWS {
let p = Gfx::hd_picture(k, view, None, false).expect("a detailed sprite for every kind");
assert!(seen.insert(p), "{k:?}: pictures do not overlap");
assert!(pic_texels(&fine, p).iter().filter(|t| t.3 > 128).count() > 100, "{k:?} view {view}");
let a = Gfx::hd_anchor(p);
assert!(a.top < a.shoulder && a.shoulder < a.bottom && a.sh.0 < a.sh.1, "{k:?} view {view}: {a:?}");
if !Gfx::hd_has_anim(k, HdAnim::Walk) {
assert_eq!(Gfx::hd_picture(k, view, Some((HdAnim::Walk, 2)), false), Some(p));
}
let uv = Gfx::hd_uv(p, false);
assert!(uv.x + uv.w <= 1.0 + 1e-6 && uv.y + uv.h <= 1.0 + 1e-6);
}
}
}
#[test]
fn small_sprites_and_sack() {
let img = gen_sprites();
assert_eq!((img.width as f32, img.height as f32), (SPRITE_TEX_W, SPRITE_TEX_H));
// every pixel a 3x3 block
let r = Gfx::sprite_cell_view(EK::Knight, false, 1, 0);
for y in 0..SPR_CELL {
for x in 0..SPR_CELL {
let p = img.get_pixel(r.x as u32 + x * SPR_K, r.y as u32 + y * SPR_K);
let q = img.get_pixel(r.x as u32 + x * SPR_K + 2, r.y as u32 + y * SPR_K + 2);
assert_eq!((p.r, p.g, p.b, p.a), (q.r, q.g, q.b, q.a));
}
}
assert!(Gfx::sprite_cell_sack(EK::Knight, false, 1, 0).is_none());
for view in 0..VIEWS {
let plain = Gfx::sprite_cell_view(EK::Worker, false, 1, view);
let sack = Gfx::sprite_cell_sack(EK::Worker, false, 1, view).unwrap();
assert!(differ(&img, sack, plain) > 30, "view {view}: the small sack shows");
}
}
// `cargo test --bins dump_imp_cells -- --ignored` writes the imp's detailed pictures (idle
// and the digging frames, without and with sack; the 64x64 level) and the gear parts to /tmp
#[test]
#[ignore]
fn dump_imp_cells() {
let (fine, _) = gen_hd_sprites();
let mut rows: Vec<Vec<u32>> = Vec::new();
for sack in [false, true] {
for anim in [None, Some((HdAnim::Hack, 0)), Some((HdAnim::Hack, 1)), Some((HdAnim::Hack, 2)), Some((HdAnim::Hack, 3))] {
rows.push((0..VIEWS).map(|v| Gfx::hd_picture(EK::Worker, v, anim, sack).unwrap()).collect());
}
}
let c = HD_CELL;
let (w, h) = (c * VIEWS, c * rows.len() as u32);
let mut out = Image::gen_image_color(w as u16, h as u16, Color::new(0.35, 0.33, 0.38, 1.0));
for (row, list) in rows.iter().enumerate() {
for (i, &pic) in list.iter().enumerate() {
let (ox, oy) = ((pic % HD_COLS64) * c, (pic / HD_COLS64) * c);
for y in 0..c {
for x in 0..c {
let p = fine.get_pixel(ox + x, oy + y);
if p.a > 0.5 {
out.set_pixel(i as u32 * c + x, h - 1 - (row as u32 * c + y), p);
}
}
}
}
}
out.export_png("/tmp/imp-cells.png");
let gear = gen_gear();
let mut g = Image::gen_image_color((GEAR_TEX * 4) as u16, (GEAR_TEX * 4) as u16, Color::new(0.35, 0.33, 0.38, 1.0));
for y in 0..GEAR_TEX * 4 {
for x in 0..GEAR_TEX * 4 {
let p = gear.get_pixel(x / 4, y / 4);
if p.a > 0.5 {
g.set_pixel(x, GEAR_TEX * 4 - 1 - y, p);
}
}
}
g.export_png("/tmp/gear.png");
}
}
#[cfg(test)]
mod doc_images {
use super::*;
// writes a picture scaled up with nearest pixels
fn save(img: &Image, src: Rect, scale: u32, path: &str) {
let (w, h) = (src.w as u32 * scale, src.h as u32 * scale);
let mut out = Image::gen_image_color(w as u16, h as u16, Color::new(0.0, 0.0, 0.0, 0.0));
for y in 0..h {
for x in 0..w {
let p = img.get_pixel(src.x as u32 + x / scale, src.y as u32 + y / scale);
// export_png writes the rows bottom up: store them flipped
out.set_pixel(x, h - 1 - y, p);
}
}
out.export_png(path);
}
// `cargo test --release export_doc_images -- --ignored` refreshes the pictures in docs/img
#[test]
#[ignore]
fn export_doc_images() {
std::fs::create_dir_all("docs/img/units").unwrap();
std::fs::create_dir_all("docs/img/icons").unwrap();
let sprites = gen_sprites();
for &k in SPRITE_KINDS.iter() {
let id = crate::game::st(k).name.trim_start_matches("unit.");
for (lv, tag) in [(1u32, "1"), (5, "5"), (10, "10")] {
save(&sprites, Gfx::sprite_cell(k, false, lv), 3, &format!("docs/img/units/{}_{}.png", id, tag));
}
}
let atlas = gen_atlas();
for (k, t) in crate::traps::TRAP_KINDS.iter().enumerate() {
save(&atlas, Gfx::cell(I_TRAPS + k as u32), 4, &format!("docs/img/icons/{}.png", t.id()));
}
for s in crate::spells::SPELLS.iter() {
save(&atlas, Gfx::cell(crate::spells::spell_icon(*s)), 4, &format!("docs/img/icons/{}.png", s.id()));
}
for (id, cell) in [("lightning", I_BOLT), ("heal", I_HEAL), ("summon", I_SUMMON), ("arrow", I_ARROW), ("cannon", I_CANNON), ("magic", I_MAGIC), ("rune", I_RUNE)] {
save(&atlas, Gfx::cell(cell), 4, &format!("docs/img/icons/{}.png", id));
}
for (k, id) in crate::game::ROOM_NAMES.iter().enumerate() {
let tex = crate::ui::tool_info(crate::ui::Tool::Room(k)).icon;
save(&atlas, Gfx::cell(tex), 4, &format!("docs/img/icons/{}.png", id.trim_start_matches("room.")));
}
}
}
#[cfg(test)]
mod font_tests {
use super::*;
// the glyph of a character as rows of '#' and '.'
fn bitmap(img: &Image, ch: char) -> Vec<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);
}
}
}