Files
dungeoncraft/src/gfx.rs
T
Richard ZinkandClaude Opus 5.5 75dc445d3b Rivers and bridges: water and lava tiles, bridge tool, burning in lava
- New tile kinds (appended for the savegame codec): water, lava, wooden bridge, stone bridge, core shard
- Sunken river beds with banks, bridge decks with rails, lava glow and lights
- Bridge tool (key U, rooms category): bridges grow from own ground, 30 gold/tile over water, 75 over lava
- Selling a bridge drops the units on it into the river (water: swim to the bank, lava: burn)
- Cannon blasts and punches knock units back; into lava = burnt (heroes still pay their bounty)
- Heroes cannot path through rivers; texts EN/DE; tests for bridges and lava

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 10:32:40 +01:00

1122 lines
38 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 = 8;
// 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 struct Gfx {
pub atlas: Texture2D,
pub sprites: Texture2D,
pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*)
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))
});
}
// 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_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
cv.cell(T_WATER + v, |x, y| {
let wave = ((x as f32 * 0.8 + (y as f32 * 0.9 + v as f32 * 1.7).sin() * 2.2) as i32 + y * 2) % 9;
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: glowing orange with dark crust plates
cv.cell(T_LAVA + v, |x, y| {
let crust = rnd(x / 3 + (y / 3) % 2, y / 3, v + 130);
let edge = rnd(x, y, v + 131) * 0.25;
if crust + edge > 0.85 {
Some(shade((60, 22, 14), (rnd(x, y, v + 132) * 14.0) as i32))
} else if crust + edge > 0.7 {
Some(c(200, 60, 20))
} else if hash(x, y, v + 133) % 11 == 0 {
Some(c(255, 230, 120))
} else {
Some(shade((250, 130, 30), (rnd(x, y, v + 134) * 30.0) as i32 - 15))
}
});
// 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
}
});
cv.img
}
// ------------------------------------------------------------------ Sprites (10x10)
const SPR: usize = 10;
const SPR_CELL: u32 = 12;
fn sprite_rows(k: EK) -> [&'static str; 10] {
match k {
EK::Worker => [
"..........",
"k..kkkk..k",
"okkooookko",
".koeooeok.",
"..kooook..",
"...kRRk..S",
"..kRRRRkSS",
"..kRRRRkb.",
"...kRRkb..",
"...k..k...",
],
EK::Kobold => [
"........s.",
"..kkkk..s.",
".kggggk.b.",
".kgegegkb.",
".kggggkkb.",
"..kGGkggb.",
".kgGGGgkb.",
".kgGGGk.b.",
"..kGkGk...",
"..kk.kk...",
],
EK::Spider => [
"..........",
"...kkkk...",
"k.kPPPPk.k",
".kPPPPPPk.",
"kkPePPePkk",
".kPPPPPPk.",
"k.kPPPPk.k",
".k.kkkk.k.",
"k........k",
"..........",
],
EK::Mage => [
"....kk....",
"...kPPk...",
"..kPPPPk..",
".kkkkkkkk.",
"..kfefek.y",
"..kPPPPk.b",
".kPpPPpPkb",
".kPPpPPPkb",
".kPPPPPPk.",
"..kkkkkk..",
],
EK::Ogre => [
"..kkkkkk..",
".kcccccck.",
".kcecceck.",
".kcCccCck.",
"kcccccccck",
"kcCcccccCk",
"kccCccCcck",
".kcccccck.",
".kcCk.kCk.",
".kkk...kk.",
],
EK::Knight => [
"...kkkk...",
"..kssssk..",
"..kSkkSk.s",
"..kssssk.s",
"..kuuuuk.s",
".ksuuuusks",
".kSuyyuSk.",
"..kuuuuk..",
"..kSkkSk..",
"..kk..kk..",
],
EK::Archer => [
"...kkkk..b",
"..kGGGGkbk",
"..kGfkfkb.",
"..kGfffkb.",
"..kgggggb.",
".kfgggggb.",
"..kggggkbk",
"..kBBBBk.b",
"..kBkkBk..",
"..kk..kk..",
],
EK::Priest => [
"....yy....",
"...kkkk...",
"..kffffk..",
"..kfkfkk..",
"..kwwwwk..",
".kwwyywwk.",
".kwwyywwk.",
".kwwwwwwk.",
".kwwwwwwk.",
"..kkkkkk..",
],
EK::Dwarf => [
"..........",
"...kkkk...",
"..kSSSSk..",
"..kSSSSkS.",
"..kfkfkkSS",
"..kooook.b",
".kboooobkb",
".kbBBBBbk.",
"..kBkkBk..",
"..kk..kk..",
],
EK::Jailer => [
"...kkkk...",
"..kCCCCk..",
"..kCkkCk..",
"..kCeeCk..",
".kCCCCCCk.",
"kCCRRRRCCk",
"kCCRRRRCCS",
".kCCCCCCkS",
"..kCk.kCkS",
"..kk...kk.",
],
EK::Priestess => [
"....oo..y.",
"...kkkk.o.",
"..kffffk..",
"..kfkfkk..",
"..kRRRRk..",
".kRRooRRk.",
".kRRooRRkf",
".kRRRRRRk.",
".kRRRRRRk.",
"..kkkkkk..",
],
// 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; 13] = [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];
// width of the sprite texture in pixels
pub const SPRITE_TEX_W: f32 = (13 * 12) as f32;
pub const SPRITE_TEX_H: f32 = 24.0;
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
}
// ------------------------------------------------------------------ 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.....",
"................",
];
const CUR_OPEN: [&str; 16] = [
"................",
".kw.kw.kw.......",
".kgkkgkkgk..kw..",
".kgkkgkkgk.kgk..",
".kgkkgkkgkkggk..",
".kgggggggggggk..",
"kkgggggggggggk..",
"kgkggggggggggk..",
"kggkgggggggGk...",
".kggggggggggk...",
"..kgggggggGk....",
"...kggggggGk....",
"...kgggggGk.....",
"....kGGGGk......",
"....kkkkkk......",
"................",
];
const CUR_FIST: [&str; 16] = [
"................",
"................",
"................",
"...kkkkkkkk.....",
"..kgkgkgkgGk....",
"..kgkgkgkgGk....",
".kkggggggggGk...",
"kggkgggggggGk...",
"kgggggggggGGk...",
".kggggggggGk....",
"..kgggggggGk....",
"...kggggggGk....",
"...kGGGGGGk.....",
"....kkkkkk......",
"................",
"................",
];
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),
'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);
// 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);
let cursors = Texture2D::from_image(&gen_cursors());
cursors.set_filter(FilterMode::Nearest);
Gfx { atlas, sprites, cursors, 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 && hero => T_FORTWALL + 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::Temple => T_TEMPLE + v,
Kind::Tower => T_CLAIM + v,
Kind::Water => T_WATER + v,
Kind::Lava => T_LAVA + v,
Kind::Bridge => T_BRIDGE + (v & 1),
Kind::LavaBridge => T_STONEBRIDGE + (v & 1),
Kind::Shard => T_HEROFLOOR + 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);
}
}
}