Files
dungeoncraft/src/gfx.rs
T

831 lines
27 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;
use macroquad::prelude::*;
pub const CELL: f32 = 16.0;
const ATLAS_COLS: u32 = 16;
const ATLAS_ROWS: u32 = 6;
// 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 struct Gfx {
pub atlas: Texture2D,
pub sprites: Texture2D,
pub font: Texture2D, // DINOBYTE, CP437, 16x16 glyphs of 6x8 px
}
// ------------------------------------------------------------------ bitmap font (CP437)
pub const GW: f32 = 6.0; // glyph width
pub const GH: f32 = 8.0; // glyph height
const CP437_LOW: &str = " ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼";
const CP437_HIGH: &str = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}";
pub fn cp437(ch: char) -> u8 {
// DINOBYTE has ö (0x94) and ò (0x95) swapped
match ch {
'ö' => return 0x95,
'ò' => return 0x94,
_ => {}
}
let u = ch as u32;
if (32..127).contains(&u) {
return u as u8;
}
if ch == '⌂' {
return 127;
}
if let Some(i) = CP437_LOW.chars().position(|c| c == ch) {
if i > 0 {
return i as u8;
}
}
if let Some(i) = CP437_HIGH.chars().position(|c| c == ch) {
return 128 + i as u8;
}
match ch {
'ẞ' => 0xE1,
'–' | '—' => b'-',
'„' | '“' | '”' => b'"',
'‚' | '‘' | '’' => b'\'',
'…' => b'.',
'×' => b'x',
_ => b'?',
}
}
// frame glyphs: [top-left, top-right, bottom-left, bottom-right, horizontal, vertical]
pub const BOX_SINGLE: [u8; 6] = [0xDA, 0xBF, 0xC0, 0xD9, 0xC4, 0xB3];
pub const BOX_DOUBLE: [u8; 6] = [0xC9, 0xBB, 0xC8, 0xBC, 0xCD, 0xBA];
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
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 stone = hash((x + off) / 8, row, v + 3) % 5;
let d = (rnd(x, y, v) * var as f32) as i32 - var / 2 + stone as i32 * 3 - 6;
// highlight top/left, shadow bottom/right
let edge = if y % 8 == 1 || sx % 8 == 1 {
10
} else if y % 8 == 7 || sx % 8 == 7 {
-8
} else {
0
};
shade(base, d + edge)
}
fn earth_px(x: i32, y: i32, v: u32) -> Color {
let base = (98, 68, 44);
let big = rnd(x / 3, y / 3, v + 11) * 16.0 - 8.0;
let fine = rnd(x, y, v) * 14.0 - 7.0;
if hash(x, y, v + 99) % 37 == 0 {
return c(150, 120, 90);
}
if hash(x, y, v + 98) % 29 == 0 {
return c(62, 42, 28);
}
shade(base, (big + fine) as i32)
}
fn gen_atlas() -> Image {
let mut cv = Canvas { img: Image::gen_image_color((ATLAS_COLS * 16) as u16, (ATLAS_ROWS * 16) as u16, Color::new(0.0, 0.0, 0.0, 0.0)) };
for v in 0..4u32 {
// rock
cv.cell(T_ROCK + v, |x, y| {
let blk = rnd(x / 4 + (y / 4) % 2, y / 4, v + 21) * 20.0 - 10.0;
let fine = rnd(x, y, v + 5) * 10.0 - 5.0;
let crack = (x + y * 3 + v as i32 * 5) % 23 == 0 || (x * 2 - y + v as i32) % 31 == 0;
if crack {
Some(c(40, 40, 48))
} else {
Some(shade((80, 80, 92), (blk + fine) as i32))
}
});
// earth
cv.cell(T_EARTH + v, |x, y| Some(earth_px(x, y, v)));
// gold
cv.cell(T_GOLD + v, |x, y| {
let mut best = 99.0f32;
for k in 0..6 {
let nx = 2 + (hash(k, 1, v + 40) % 12) as i32;
let ny = 2 + (hash(k, 2, v + 40) % 12) as i32;
let d = (((x - nx) * (x - nx) + (y - ny) * (y - ny)) as f32).sqrt();
best = best.min(d - if k % 2 == 0 { 0.5 } else { 0.0 });
}
if best < 0.6 {
Some(c(255, 240, 150))
} else if best < 1.4 {
Some(c(240, 196, 52))
} else if best < 2.0 {
Some(c(168, 118, 30))
} else {
Some(earth_px(x, y, v + 60))
}
});
cv.cell(T_FLOOR + v, |x, y| Some(stones(x, y, v, (64, 56, 50), (34, 30, 28), 10)));
cv.cell(T_CLAIM + v, |x, y| {
let col = stones(x, y, v + 10, (78, 44, 58), (36, 20, 28), 10);
// small emblem in the middle of every other stone
if (x % 8 == 4 && y % 8 == 4) && hash(x / 8, y / 8, v) % 2 == 0 {
Some(c(150, 60, 80))
} else {
Some(col)
}
});
cv.cell(T_HEROFLOOR + v, |x, y| Some(stones(x, y, v + 20, (132, 136, 150), (80, 84, 96), 12)));
cv.cell(T_TREASURY + v, |x, y| {
let chk = ((x / 4) + (y / 4)) % 2 == 0;
let base = if chk { (120, 92, 36) } else { (100, 74, 30) };
let d = (rnd(x, y, v + 30) * 10.0) as i32 - 5;
if x % 4 == 0 || y % 4 == 0 {
Some(shade((70, 52, 22), d))
} else {
Some(shade(base, d))
}
});
cv.cell(T_LAIR + v, |x, y| {
let streak = ((x * 2 + y + (hash(y, 0, v) % 3) as i32) % 5) as i32;
let d = (rnd(x, y, v + 31) * 18.0) as i32 - 9 + streak * 4;
Some(shade((112, 86, 46), d))
});
cv.cell(T_FARM + v, |x, y| {
let moss = rnd(x / 2, y / 2, v + 32);
let d = (rnd(x, y, v + 33) * 14.0) as i32 - 7;
if moss > 0.55 {
Some(shade((58, 98, 40), d))
} else {
Some(shade((62, 46, 32), d))
}
});
cv.cell(T_TRAINING + v, |x, y| {
let plank = y / 4;
let off = (hash(plank, 0, v + 34) % 16) as i32;
if y % 4 == 0 || (x + off) % 16 == 0 {
Some(c(58, 38, 24))
} else {
let grain = ((x + plank * 5) % 7 == 0) as i32 * -10;
Some(shade((116, 80, 48), (rnd(x, y, v + 35) * 10.0) as i32 + grain))
}
});
cv.cell(T_LIBRARY + v, |x, y| {
if x >= 3 && x <= 12 && y >= 3 && y <= 12 {
let border = x == 3 || x == 12 || y == 3 || y == 12;
if border {
Some(c(170, 130, 50))
} else {
Some(shade((116, 30, 42), (rnd(x, y, v + 36) * 10.0) as i32))
}
} else {
let plank = x / 4;
if x % 4 == 0 {
Some(c(40, 24, 16))
} else {
Some(shade((74, 46, 32), (rnd(x, y, v + 37) * 10.0) as i32 + (plank % 2) * 4))
}
}
});
cv.cell(T_COREFLOOR + v, |x, y| Some(stones(x, y, v + 40, (86, 32, 34), (40, 14, 16), 10)));
cv.cell(T_PORTALFLOOR + v, |x, y| Some(stones(x, y, v + 50, (66, 44, 92), (32, 20, 46), 10)));
cv.cell(T_GATEFLOOR + v, |x, y| {
let vein = ((x as f32 * 0.7 + (y as f32 * 0.5).sin() * 3.0 + v as f32 * 4.0) as i32) % 9 == 0;
if vein {
Some(c(150, 150, 165))
} else {
Some(shade((196, 196, 206), (rnd(x, y, v + 60) * 12.0) as i32 - 6))
}
});
}
// wall faces
cv.cell(T_FACE_EARTH, |x, y| {
let streak = (hash(x, 0, 70) % 3) as i32 * 5;
Some(shade((62, 42, 28), (rnd(x, y, 71) * 10.0) as i32 - streak - y / 2))
});
cv.cell(T_FACE_ROCK, |x, y| {
let streak = (hash(x, 0, 72) % 3) as i32 * 5;
Some(shade((54, 54, 64), (rnd(x, y, 73) * 10.0) as i32 - streak - y / 2))
});
cv.cell(T_FACE_GOLD, |x, y| {
if hash(x, y, 74) % 9 == 0 {
Some(c(200, 160, 40))
} else {
Some(shade((62, 42, 28), (rnd(x, y, 75) * 10.0) as i32 - y / 2))
}
});
// dig marker: diagonal stripes
cv.cell(T_MARK, |x, y| {
if (x + y) % 6 < 2 {
Some(Color::from_rgba(255, 220, 60, 150))
} else if x == 0 || y == 0 || x == 15 || y == 15 {
Some(Color::from_rgba(255, 220, 60, 200))
} else {
None
}
});
// bed
cv.cell(D_BED, |x, y| {
let dx = (x as f32 - 7.5) / 6.5;
let dy = (y as f32 - 8.0) / 5.5;
let d = dx * dx + dy * dy;
if d < 0.55 {
Some(if x < 5 { c(200, 190, 160) } else { c(110, 50, 60) })
} else if d < 1.0 {
Some(c(150, 118, 64))
} else {
None
}
});
// mushrooms
cv.cell(D_MUSHROOM, |x, y| {
let caps = [(4, 6, 3), (11, 5, 2), (8, 11, 3)];
for &(cx, cy, r) in &caps {
let dx = x - cx;
let dy = y - cy;
if dy <= 0 && dx * dx + dy * dy * 2 <= r * r + 1 {
return Some(if (x + y) % 3 == 0 && dy < 0 { c(250, 230, 220) } else { c(200, 40, 50) });
}
if dx.abs() <= 0 && dy > 0 && dy <= r {
return Some(c(230, 220, 200));
}
}
None
});
// training dummy
cv.cell(D_DUMMY, |x, y| {
if (x == 7 || x == 8) && y >= 4 {
Some(c(90, 60, 36))
} else if y == 7 && (3..=12).contains(&x) {
Some(c(90, 60, 36))
} else if (x - 7).pow(2) + (y - 3).pow(2) <= 5 {
Some(c(200, 170, 110))
} else if y >= 14 && (4..=11).contains(&x) {
Some(c(60, 40, 24))
} else {
None
}
});
// gold pile
cv.cell(D_COINS, |x, y| {
let dx = (x as f32 - 7.5) / 7.0;
let dy = (y as f32 - 11.0) / 5.0;
let d = dx * dx + dy * dy;
if d < 1.0 && y <= 14 {
let hl = hash(x, y, 80) % 5 == 0;
Some(if hl { c(255, 245, 170) } else if d > 0.6 { c(190, 140, 30) } else { c(242, 200, 60) })
} else {
None
}
});
// bookshelf
cv.cell(D_SHELF, |x, y| {
if y > 9 || x == 0 || x == 15 {
return if y <= 10 { Some(c(60, 36, 22)) } else { None };
}
if y == 0 || y == 5 {
return Some(c(60, 36, 22));
}
let book = hash(x, y / 5, 81) % 5;
Some(match book {
0 => c(150, 40, 40),
1 => c(40, 80, 140),
2 => c(60, 120, 60),
3 => c(160, 130, 50),
_ => c(100, 60, 120),
})
});
// reinforced walls: masonry in dark stone with the player's red mortar
for v in 0..4u32 {
cv.cell(T_REINF + v, |x, y| {
let row = y / 4;
let off = if row % 2 == 1 { 4 } else { 0 };
if y % 4 == 3 || (x + off) % 8 == 7 {
return Some(shade((70, 26, 30), (rnd(x, y, v + 90) * 8.0) as i32));
}
let brick = hash((x + off) / 8, row, v + 91) % 4;
Some(shade((84, 76, 74), (rnd(x, y, v + 92) * 14.0) as i32 - 7 + brick as i32 * 5 - 6 + if y % 4 == 0 { 10 } else { 0 }))
});
// torture chamber: dark stones with rusty iron grates
cv.cell(T_TORTURE + v, |x, y| {
let grate = (4..=11).contains(&x) && (4..=11).contains(&y) && (x % 3 == 1 || y % 3 == 1);
if grate {
return Some(shade((96, 44, 30), (rnd(x, y, v + 93) * 12.0) as i32));
}
Some(stones(x, y, v + 94, (52, 40, 44), (26, 16, 18), 10))
});
}
// 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_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
}
});
cv.img
}
// ------------------------------------------------------------------ Sprites (10x10)
const SPR: usize = 10;
const SPR_CELL: u32 = 12;
fn sprite_rows(k: EK) -> [&'static str; 10] {
match k {
EK::Worker => [
"..........",
"k..kkkk..k",
"okkooookko",
".koeooeok.",
"..kooook..",
"...kRRk..S",
"..kRRRRkSS",
"..kRRRRkb.",
"...kRRkb..",
"...k..k...",
],
EK::Kobold => [
"........s.",
"..kkkk..s.",
".kggggk.b.",
".kgegegkb.",
".kggggkkb.",
"..kGGkggb.",
".kgGGGgkb.",
".kgGGGk.b.",
"..kGkGk...",
"..kk.kk...",
],
EK::Spider => [
"..........",
"...kkkk...",
"k.kPPPPk.k",
".kPPPPPPk.",
"kkPePPePkk",
".kPPPPPPk.",
"k.kPPPPk.k",
".k.kkkk.k.",
"k........k",
"..........",
],
EK::Mage => [
"....kk....",
"...kPPk...",
"..kPPPPk..",
".kkkkkkkk.",
"..kfefek.y",
"..kPPPPk.b",
".kPpPPpPkb",
".kPPpPPPkb",
".kPPPPPPk.",
"..kkkkkk..",
],
EK::Ogre => [
"..kkkkkk..",
".kcccccck.",
".kcecceck.",
".kcCccCck.",
"kcccccccck",
"kcCcccccCk",
"kccCccCcck",
".kcccccck.",
".kcCk.kCk.",
".kkk...kk.",
],
EK::Knight => [
"...kkkk...",
"..kssssk..",
"..kSkkSk.s",
"..kssssk.s",
"..kuuuuk.s",
".ksuuuusks",
".kSuyyuSk.",
"..kuuuuk..",
"..kSkkSk..",
"..kk..kk..",
],
EK::Archer => [
"...kkkk..b",
"..kGGGGkbk",
"..kGfkfkb.",
"..kGfffkb.",
"..kgggggb.",
".kfgggggb.",
"..kggggkbk",
"..kBBBBk.b",
"..kBkkBk..",
"..kk..kk..",
],
EK::Priest => [
"....yy....",
"...kkkk...",
"..kffffk..",
"..kfkfkk..",
"..kwwwwk..",
".kwwyywwk.",
".kwwyywwk.",
".kwwwwwwk.",
".kwwwwwwk.",
"..kkkkkk..",
],
EK::Dwarf => [
"..........",
"...kkkk...",
"..kSSSSk..",
"..kSSSSkS.",
"..kfkfkkSS",
"..kooook.b",
".kboooobkb",
".kbBBBBbk.",
"..kBkkBk..",
"..kk..kk..",
],
EK::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),
_ => return None,
})
}
pub const SPRITE_KINDS: [EK; 10] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Paladin];
pub fn sprite_index(k: EK) -> u32 {
SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32
}
// row 0: normal sprites, row 1: white silhouettes (hit flash)
fn gen_sprites() -> Image {
let n = SPRITE_KINDS.len() as u32;
let mut img = Image::gen_image_color((n * SPR_CELL) as u16, (2 * SPR_CELL) as u16, Color::new(0.0, 0.0, 0.0, 0.0));
for (i, &k) in SPRITE_KINDS.iter().enumerate() {
let rows = sprite_rows(k);
for (y, row) in rows.iter().enumerate() {
for (x, ch) in row.chars().enumerate().take(SPR) {
if let Some(col) = pal(ch) {
let px = i as u32 * SPR_CELL + 1 + x as u32;
let py = 1 + y as u32;
img.set_pixel(px, py, col);
img.set_pixel(px, py + SPR_CELL, WHITE);
}
}
}
}
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);
// load the font; transparent background, white glyphs (for tinting)
let mut img = Image::from_file_with_format(include_bytes!("../assets/dbyte_1x.png"), Some(ImageFormat::Png)).expect("Schrift");
for px in img.get_image_data_mut().iter_mut() {
let on = px[3] > 128 && px[0] > 128;
*px = if on { [255, 255, 255, 255] } else { [0, 0, 0, 0] };
}
let font = Texture2D::from_image(&img);
font.set_filter(FilterMode::Nearest);
Gfx { atlas, sprites, font }
}
pub fn cell(id: u32) -> Rect {
Rect::new((id % ATLAS_COLS) as f32 * CELL, (id / ATLAS_COLS) as f32 * CELL, CELL, CELL)
}
pub fn sprite_rect(k: EK, white: bool) -> Rect {
let i = sprite_index(k);
Rect::new((i * SPR_CELL + 1) as f32, (1 + if white { SPR_CELL } else { 0 }) as f32, SPR as f32, SPR as f32)
}
pub fn tile_tex(kind: Kind, claimed: bool, hero: bool, var: u8, reinforced: bool) -> u32 {
let v = var as u32 & 3;
match kind {
Kind::Rock => T_ROCK + v,
Kind::Earth if reinforced => T_REINF + v,
Kind::Earth => T_EARTH + v,
Kind::Gold => T_GOLD + v,
Kind::Floor => {
if claimed {
T_CLAIM + 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::Tower => T_CLAIM + v,
}
}
pub fn draw_cell(&self, id: u32, x: f32, y: f32, w: f32, h: f32, color: Color) {
draw_texture_ex(
&self.atlas,
x,
y,
color,
DrawTextureParams { dest_size: Some(vec2(w, h)), source: Some(Self::cell(id)), ..Default::default() },
);
}
// draw a single CP437 glyph (x, y = top-left corner)
pub fn glyph(&self, code: u8, x: f32, y: f32, scale: f32, color: Color) {
let src = Rect::new((code % 16) as f32 * GW, (code / 16) as f32 * GH, 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(cp437(ch), cx, y, scale, color);
}
cx += GW * scale;
}
}
pub fn measure(&self, s: &str, scale: f32) -> f32 {
s.chars().count() as f32 * GW * scale
}
// 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 CP437 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 tx = x + 2.0 * cw;
draw_rectangle(tx, y, self.measure(&t, scale), GH * scale, bg);
self.text(&t, tx, y, scale, tcol);
}
}
}