- 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>
1122 lines
38 KiB
Rust
1122 lines
38 KiB
Rust
// 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);
|
||
}
|
||
}
|
||
}
|