Files
Verbatim/src/ui/mod.rs
T
Emil a2db99f900 feat: toggleable inventory overlay with mouse navigation
- Press Tab to open/close inventory overlay
- 4x2 grid of item slots (8 slots) centered on screen
- Each slot shows 2-char glyph + item name label
- Mouse hover highlights slot with golden border
- Hover shows action hint (Use/Equip/Equipped)
- Left-click slot: use/equip item at that index
- Right-click slot: drop item at that index
- Equipped items shown at bottom of panel
- Character panel hidden when inventory is open (saves space)
- Shooting disabled while inventory is open (prevents accidental fire)
- Mouse position tracked in UI coordinates for slot detection

All 171 tests + 14 scenarios pass.
2026-06-21 18:51:45 +03:00

941 lines
32 KiB
Rust

use std::collections::HashMap;
use crate::entity::entity::{Entity, EntityKind};
use crate::world::cell::MaterialId;
pub const UI_SCALE: i32 = 4;
#[derive(Clone, Debug)]
pub struct UiCell {
pub ch: char,
pub fg: [u8; 3],
pub bg: [u8; 3],
pub alpha: u8,
}
pub struct UiLayer {
cells: HashMap<(i32, i32), UiCell>,
messages: Vec<(String, u32)>,
damage_numbers: Vec<DamageNumber>,
}
#[derive(Clone)]
pub struct DamageNumber {
pub x: f32,
pub y: f32,
pub text: String,
pub life: u32,
pub max_life: u32,
}
impl UiLayer {
pub fn new() -> Self {
Self {
cells: HashMap::new(),
messages: Vec::new(),
damage_numbers: Vec::new(),
}
}
pub fn clear(&mut self) {
self.cells.clear();
self.damage_numbers.retain(|d| d.life > 0);
for d in &mut self.damage_numbers {
d.y -= 0.1;
d.life -= 1;
}
}
pub fn set(&mut self, x: i32, y: i32, ch: char, fg: [u8; 3], bg: [u8; 3]) {
self.cells.insert(
(x, y),
UiCell {
ch,
fg,
bg,
alpha: 255,
},
);
}
pub fn set_alpha(&mut self, x: i32, y: i32, ch: char, fg: [u8; 3], bg: [u8; 3], alpha: u8) {
self.cells.insert((x, y), UiCell { ch, fg, bg, alpha });
}
pub fn get(&self, x: i32, y: i32) -> Option<&UiCell> {
self.cells.get(&(x, y))
}
pub fn keys(&self) -> impl Iterator<Item = &(i32, i32)> {
self.cells.keys()
}
pub fn add_message(&mut self, text: &str) {
self.messages.push((text.to_string(), 300));
}
pub fn add_damage_number(&mut self, x: f32, y: f32, text: &str) {
self.damage_numbers.push(DamageNumber {
x,
y,
text: text.to_string(),
life: 50,
max_life: 50,
});
}
pub fn draw_damage_numbers(&mut self, cam_x: i32, cam_y: i32) {
let numbers: Vec<DamageNumber> = self.damage_numbers.clone();
for d in numbers {
let sx = (d.x as i32 - cam_x) * UI_SCALE;
let sy = (d.y as i32 - cam_y) * UI_SCALE;
let fg = if d.text.starts_with('+') {
[80, 240, 80]
} else {
[255, 80, 80]
};
self.draw_text(sx, sy, &d.text, fg, 255);
}
}
pub fn draw_edge_indicators(
&mut self,
screen_w: usize,
screen_h: usize,
entities: &[Entity],
cam_x: i32,
cam_y: i32,
) {
let sw = screen_w as i32;
let sh = screen_h as i32;
for e in entities {
if !e.alive {
continue;
}
let (sx, sy) = entity_screen_pos_ui(e, cam_x, cam_y);
if sx >= 0 && sx < sw && sy >= 0 && sy < sh {
continue;
}
let dx = sx - sw / 2;
let dy = sy - sh / 2;
let dist = ((dx * dx + dy * dy) as f32).sqrt().max(1.0);
let ix = (sw / 2) as f32 + (dx as f32 / dist) * (sw as f32 / 2.0 - 2.0);
let iy = (sh / 2) as f32 + (dy as f32 / dist) * (sh as f32 / 2.0 - 2.0);
let (ix, iy) = (
ix.clamp(1.0, sw as f32 - 2.0) as i32,
iy.clamp(1.0, sh as f32 - 2.0) as i32,
);
let ch = match e.kind {
EntityKind::Goblin => 'g',
EntityKind::Slime => 's',
EntityKind::Player => '@',
EntityKind::Corpse => '%',
};
let fg = match e.kind {
EntityKind::Goblin => [160, 240, 120],
EntityKind::Slime => [120, 240, 160],
EntityKind::Player => [200, 180, 255],
EntityKind::Corpse => [160, 160, 160],
};
self.set(ix, iy, ch, fg, [20, 20, 30]);
}
}
pub fn tick_messages(&mut self) {
for (_, life) in &mut self.messages {
if *life > 0 {
*life -= 1;
}
}
self.messages.retain(|(_, life)| *life > 0);
while self.messages.len() > 8 {
self.messages.remove(0);
}
}
pub fn messages(&self) -> &[(String, u32)] {
&self.messages
}
pub fn damage_numbers(&self) -> &[DamageNumber] {
&self.damage_numbers
}
pub fn draw_health_bar(
&mut self,
screen_x: i32,
screen_y: i32,
health: f32,
max_health: f32,
width: i32,
) {
if max_health <= 0.0 {
return;
}
let ratio = (health / max_health).clamp(0.0, 1.0);
let filled = (ratio * width as f32).round() as i32;
let color = if ratio > 0.6 {
[60, 220, 60]
} else if ratio > 0.3 {
[240, 200, 40]
} else {
[240, 50, 50]
};
for i in 0..width {
let x = screen_x + i;
let ch = if i < filled { '#' } else { '-' };
let fg = if i < filled { color } else { [80, 80, 80] };
self.set(x, screen_y, ch, fg, [0, 0, 0]);
}
}
pub fn draw_entity_labels(&mut self, entities: &[Entity], cam_x: i32, cam_y: i32) {
for e in entities {
if !e.alive || e.kind == EntityKind::Corpse {
continue;
}
let (sx, sy) = entity_screen_pos_ui(e, cam_x, cam_y);
let label = e.name().to_uppercase();
let top = sy - (e.half_h as i32 * UI_SCALE) - 7;
let x = sx - (Self::text_width(&label) / 2);
let fg = entity_kind_color(e);
self.draw_text(x, top, &label, fg, 255);
}
}
pub fn draw_status_icons(&mut self, entities: &[Entity], cam_x: i32, cam_y: i32) {
for e in entities {
if !e.alive {
continue;
}
let (sx, sy) = entity_screen_pos_ui(e, cam_x, cam_y);
let icon_x = sx + (e.half_w as i32 * UI_SCALE) + 1;
let mut icon_y = sy - (e.half_h as i32 * UI_SCALE) - 7;
if e.on_fire {
self.set(icon_x, icon_y, 'F', [255, 100, 20], [0, 0, 0]);
icon_y += 1;
}
if e.poisoned {
self.set(icon_x, icon_y, 'P', [80, 255, 80], [0, 0, 0]);
icon_y += 1;
}
if e.frozen {
self.set(icon_x, icon_y, 'I', [120, 220, 255], [0, 0, 0]);
icon_y += 1;
}
if e.bleeding {
self.set(icon_x, icon_y, 'B', [255, 40, 40], [0, 0, 0]);
}
}
}
pub fn draw_items(&mut self, items: &[crate::entity::item::Item], cam_x: i32, cam_y: i32) {
for item in items {
let sx = (item.x - cam_x) * UI_SCALE;
let sy = (item.y - cam_y) * UI_SCALE;
self.set(sx, sy, item.display_char(), item.color(), [0, 0, 0]);
}
}
pub fn draw_minimap(
&mut self,
screen_w: usize,
screen_h: usize,
grid: &crate::world::grid::Grid,
entities: &[Entity],
cam_x: i32,
cam_y: i32,
) {
let size = 16;
let scale = 16;
let start_x = screen_w as i32 - size - 2;
let start_y = 1;
let view_w = screen_w as i32 / UI_SCALE;
let view_h = screen_h as i32 / UI_SCALE;
let cx = (cam_x + view_w / 2) / scale;
let cy = (cam_y + view_h / 2) / scale;
for dy in 0..size {
for dx in 0..size {
let mx = cx - size / 2 + dx;
let my = cy - size / 2 + dy;
let wx = mx * scale;
let wy = my * scale;
if !grid.in_bounds(wx, wy) {
self.set(start_x + dx, start_y + dy, ' ', [0, 0, 0], [20, 20, 30]);
continue;
}
let mut r = 0u32;
let mut g = 0u32;
let mut b = 0u32;
let mut n = 0u32;
for yy in 0..scale {
for xx in 0..scale {
let cell = grid.get(wx + xx, wy + yy);
if cell.material != MaterialId::Empty {
let c = cell.fg;
r += c[0] as u32;
g += c[1] as u32;
b += c[2] as u32;
n += 1;
}
}
}
let fg = if n > 0 {
[(r / n) as u8, (g / n) as u8, (b / n) as u8]
} else {
[40, 40, 50]
};
let bg = [20, 20, 30];
self.set(start_x + dx, start_y + dy, '.', fg, bg);
}
}
for e in entities {
if !e.alive {
continue;
}
let (ex, ey) = e.center();
let mx = (ex as i32 / scale) - cx + size / 2;
let my = (ey as i32 / scale) - cy + size / 2;
if mx >= 0 && mx < size && my >= 0 && my < size {
let ch = match e.kind {
EntityKind::Player => '@',
EntityKind::Goblin => 'g',
EntityKind::Slime => 's',
EntityKind::Corpse => '%',
};
let fg = entity_kind_color(e);
self.set(start_x + mx, start_y + my, ch, fg, [0, 0, 0]);
}
}
for dx in 0..size {
self.set(start_x + dx, start_y - 1, '-', [80, 80, 100], [0, 0, 0]);
self.set(start_x + dx, start_y + size, '-', [80, 80, 100], [0, 0, 0]);
}
for dy in 0..size {
self.set(start_x - 1, start_y + dy, '|', [80, 80, 100], [0, 0, 0]);
self.set(start_x + size, start_y + dy, '|', [80, 80, 100], [0, 0, 0]);
}
self.set(start_x - 1, start_y - 1, '+', [80, 80, 100], [0, 0, 0]);
self.set(start_x + size, start_y - 1, '+', [80, 80, 100], [0, 0, 0]);
self.set(start_x - 1, start_y + size, '+', [80, 80, 100], [0, 0, 0]);
self.set(
start_x + size,
start_y + size,
'+',
[80, 80, 100],
[0, 0, 0],
);
}
pub fn draw_character_panel(
&mut self,
start_x: i32,
start_y: i32,
player: Option<&Entity>,
player_state: &crate::entity::player::Player,
) {
let w = 44i32;
let h = 44i32;
let bg = [22u8, 26, 38];
let border = [160u8, 180, 255];
let title = [220u8, 230, 255];
let dim = [150u8, 160, 190];
let fg = [240u8, 240, 250];
let fill_alpha = 140u8;
let border_alpha = 220u8;
for y in 0..h {
for x in 0..w {
self.set_alpha(start_x + x, start_y + y, ' ', [0, 0, 0], bg, fill_alpha);
}
}
for x in 0..w {
if (start_x + x) % 2 == 0 {
self.set_alpha(start_x + x, start_y, '.', border, bg, border_alpha);
self.set_alpha(start_x + x, start_y + h - 1, '.', border, bg, border_alpha);
}
}
for y in 0..h {
if (start_y + y) % 2 == 0 {
self.set_alpha(start_x, start_y + y, '.', border, bg, border_alpha);
self.set_alpha(start_x + w - 1, start_y + y, '.', border, bg, border_alpha);
}
}
self.set_alpha(start_x, start_y, '*', border, bg, border_alpha);
self.set_alpha(start_x + w - 1, start_y, '*', border, bg, border_alpha);
self.set_alpha(start_x, start_y + h - 1, '*', border, bg, border_alpha);
self.set_alpha(
start_x + w - 1,
start_y + h - 1,
'*',
border,
bg,
border_alpha,
);
let title_text = "STATUS";
let tx = start_x + (w - Self::text_width(title_text)) / 2;
self.draw_text(tx, start_y + 1, title_text, title, 255);
if let Some(p) = player {
let hp_ratio = (p.health / p.max_health).clamp(0.0, 1.0);
let hp_filled = (hp_ratio * 32.0).round() as i32;
let bar_color = if hp_ratio > 0.6 {
[80, 240, 80]
} else if hp_ratio > 0.3 {
[240, 220, 60]
} else {
[255, 60, 60]
};
self.draw_text(start_x + 2, start_y + 7, "HP", fg, 255);
for i in 0..32 {
let ch = if i < hp_filled { '#' } else { '-' };
let c = if i < hp_filled {
bar_color
} else {
[80, 80, 100]
};
self.set(start_x + 10 + i, start_y + 7, ch, c, bg);
}
let hp_text = format!("{}/{} LV:{}", p.health as i32, p.max_health as i32, p.level);
self.draw_text(start_x + 2, start_y + 13, &hp_text, fg, 255);
let xp_ratio = (p.xp as f32 / p.xp_to_level() as f32).clamp(0.0, 1.0);
let xp_filled = (xp_ratio * 32.0).round() as i32;
self.draw_text(start_x + 2, start_y + 19, "XP", fg, 255);
for i in 0..32 {
let ch = if i < xp_filled { '#' } else { '-' };
let c = if i < xp_filled {
[80, 160, 240]
} else {
[60, 60, 80]
};
self.set(start_x + 10 + i, start_y + 19, ch, c, bg);
}
let stats = format!("STR:{} AGI:{}", p.strength, p.agility);
self.draw_text(start_x + 2, start_y + 25, &stats, dim, 255);
let stats2 = format!("TOU:{} WIL:{}", p.toughness, p.willpower);
self.draw_text(start_x + 2, start_y + 31, &stats2, dim, 255);
}
let inv_title = "INV:";
self.draw_text(start_x + 2, start_y + 37, inv_title, fg, 255);
let mut ix = start_x + 14;
for item in player_state.inventory.iter().take(8) {
if ix + 1 >= start_x + w - 1 {
break;
}
let [ch1, ch2] = item.display_glyph();
let col = item.color();
self.set(ix, start_y + 38, ch1, col, bg);
self.set(
ix + 1,
start_y + 38,
ch2,
[col[0] / 2, col[1] / 2, col[2] / 2],
bg,
);
ix += 3;
}
let status = if let Some(p) = player {
let mut s = String::new();
if p.on_fire {
s.push_str("[FIRE] ");
}
if p.poisoned {
s.push_str("[PSN] ");
}
if p.frozen {
s.push_str("[ICE] ");
}
if p.bleeding {
s.push_str("[BLD] ");
}
s
} else {
String::new()
};
if !status.is_empty() {
for (i, ch) in status.chars().enumerate() {
self.set(start_x + 30 + i as i32, start_y + 38, ch, [255, 80, 80], bg);
}
}
}
pub fn draw_inventory_overlay(
&mut self,
screen_w: usize,
screen_h: usize,
player_state: &crate::entity::player::Player,
mouse_ui_x: i32,
mouse_ui_y: i32,
) -> Vec<(usize, i32, i32)> {
let bg = [15u8, 18, 28];
let border = [160u8, 180, 255];
let title_col = [220u8, 230, 255];
let dim = [120u8, 130, 160];
let slot_bg = [30u8, 36, 50];
let slot_border = [80u8, 100, 140];
let hover_border = [255u8, 220, 100];
let cols = 4i32;
let rows = 2i32;
let slot_w = 8i32;
let slot_h = 6i32;
let gap = 2i32;
let panel_w = cols * slot_w + (cols + 1) * gap + 4;
let panel_h = rows * slot_h + (rows + 1) * gap + 20;
let px_start = (screen_w as i32 - panel_w) / 2;
let py_start = (screen_h as i32 - panel_h) / 2;
for y in 0..panel_h {
for x in 0..panel_w {
self.set_alpha(px_start + x, py_start + y, ' ', [0, 0, 0], bg, 200);
}
}
for x in 0..panel_w {
if x % 2 == 0 {
self.set_alpha(px_start + x, py_start, '.', border, bg, 240);
self.set_alpha(px_start + x, py_start + panel_h - 1, '.', border, bg, 240);
}
}
for y in 0..panel_h {
if y % 2 == 0 {
self.set_alpha(px_start, py_start + y, '.', border, bg, 240);
self.set_alpha(px_start + panel_w - 1, py_start + y, '.', border, bg, 240);
}
}
self.set_alpha(px_start, py_start, '*', border, bg, 240);
self.set_alpha(px_start + panel_w - 1, py_start, '*', border, bg, 240);
self.set_alpha(px_start, py_start + panel_h - 1, '*', border, bg, 240);
self.set_alpha(
px_start + panel_w - 1,
py_start + panel_h - 1,
'*',
border,
bg,
240,
);
let title = "INVENTORY";
self.draw_text(px_start + 4, py_start + 3, title, title_col, 255);
let count_text = format!("({}/{} items)", player_state.inventory.len(), cols * rows);
self.draw_text(
px_start + 4 + Self::text_width(title) + 2,
py_start + 3,
&count_text,
dim,
200,
);
let mut hover_slot: Option<usize> = None;
let mut slots: Vec<(usize, i32, i32)> = Vec::new();
for row in 0..rows {
for col in 0..cols {
let idx = (row * cols + col) as usize;
let sx = px_start + gap + col * (slot_w + gap) + 2;
let sy = py_start + 10 + row * (slot_h + gap) + gap;
for y in 0..slot_h {
for x in 0..slot_w {
self.set_alpha(sx + x, sy + y, ' ', [0, 0, 0], slot_bg, 200);
}
}
let sb = if mouse_ui_x >= sx
&& mouse_ui_x < sx + slot_w
&& mouse_ui_y >= sy
&& mouse_ui_y < sy + slot_h
{
hover_slot = Some(idx);
hover_border
} else {
slot_border
};
for x in 0..slot_w {
self.set_alpha(sx + x, sy, '.', sb, slot_bg, 240);
self.set_alpha(sx + x, sy + slot_h - 1, '.', sb, slot_bg, 240);
}
for y in 0..slot_h {
self.set_alpha(sx, sy + y, '.', sb, slot_bg, 240);
self.set_alpha(sx + slot_w - 1, sy + y, '.', sb, slot_bg, 240);
}
if idx < player_state.inventory.len() {
let item = &player_state.inventory[idx];
let [ch1, ch2] = item.display_glyph();
let col = item.color();
let cx = sx + slot_w / 2 - 1;
let cy = sy + slot_h / 2 - 1;
self.set(cx, cy, ch1, col, slot_bg);
self.set(
cx + 1,
cy,
ch2,
[col[0] / 2, col[1] / 2, col[2] / 2],
slot_bg,
);
let label = item.name();
let label_w = Self::text_width(label) as i32;
let lx = sx + (slot_w - label_w) / 2;
let ly = sy + slot_h - 1;
if lx >= sx && lx + label_w <= sx + slot_w {
self.draw_text(lx, ly, label, dim, 200);
}
if hover_slot == Some(idx) {
let action = if item.is_consumable() {
"L-click: Use"
} else if item.is_weapon() {
if player_state
.weapon
.as_ref()
.map(|w| w.typ == item.typ)
.unwrap_or(false)
{
"Equipped"
} else {
"L-click: Equip"
}
} else if item.is_armor() {
if player_state
.armor
.as_ref()
.map(|a| a.typ == item.typ)
.unwrap_or(false)
{
"Equipped"
} else {
"L-click: Equip"
}
} else {
"L-click: Use"
};
self.draw_text(sx, sy + slot_h, action, hover_border, 240);
}
} else if hover_slot == Some(idx) {
self.draw_text(sx + 1, sy + slot_h / 2, "empty", dim, 200);
}
slots.push((idx, sx, sy));
}
}
if player_state.weapon.is_some() || player_state.armor.is_some() {
let eq_y = py_start + panel_h - 6;
self.draw_text(px_start + 4, eq_y, "EQ:", dim, 200);
let mut eq_x = px_start + 8;
if let Some(ref w) = player_state.weapon {
let [c1, c2] = w.display_glyph();
let col = w.color();
self.set(eq_x, eq_y, c1, col, bg);
self.set(eq_x + 1, eq_y, c2, [col[0] / 2, col[1] / 2, col[2] / 2], bg);
eq_x += 4;
}
if let Some(ref a) = player_state.armor {
let [c1, c2] = a.display_glyph();
let col = a.color();
self.set(eq_x, eq_y, c1, col, bg);
self.set(eq_x + 1, eq_y, c2, [col[0] / 2, col[1] / 2, col[2] / 2], bg);
}
}
self.draw_text(
px_start + 4,
py_start + panel_h - 3,
"Tab: close L-click: use R-click: drop",
dim,
200,
);
slots
}
pub fn draw_hud(
&mut self,
screen_w: usize,
screen_h: usize,
player: Option<&Entity>,
tick: u64,
brush: MaterialId,
kills: u32,
score: u32,
depth: u32,
player_state: &crate::entity::player::Player,
fps: f32,
) {
let bg = [18u8, 20, 30];
let border = [160u8, 170, 210];
let fill_alpha = 140u8;
let brush_name = material_name(brush);
let weapon_glyph = player_state
.weapon
.as_ref()
.map(|i| i.display_string())
.unwrap_or(" ".to_string());
let armor_glyph = player_state
.armor
.as_ref()
.map(|i| i.display_string())
.unwrap_or(" ".to_string());
let bar_h = 18i32;
let y_top = screen_h as i32 - bar_h;
let y_row1 = y_top + 1;
let y_row2 = y_top + 7;
let y_row3 = y_top + 13;
for row in y_top..screen_h as i32 {
for x in 0..screen_w {
self.set_alpha(x as i32, row, ' ', [200, 200, 200], bg, fill_alpha);
}
}
for x in 0..screen_w {
if x % 2 == 0 {
self.set_alpha(x as i32, y_top - 1, '.', border, bg, 220);
}
}
self.draw_text(0, y_row1, "HP", [220, 220, 220], 255);
if let Some(p) = player {
let hp_ratio = (p.health / p.max_health).clamp(0.0, 1.0);
let hp_filled = (hp_ratio * 28.0).round() as i32;
let bar_fg = if hp_ratio > 0.6 {
[80, 240, 80]
} else if hp_ratio > 0.3 {
[240, 220, 60]
} else {
[255, 60, 60]
};
for i in 0..28 {
let ch = if i < hp_filled { '#' } else { '-' };
let c = if i < hp_filled { bar_fg } else { [70, 70, 90] };
self.set(10 + i, y_row1 + 4, ch, c, bg);
}
let hp_text = format!("{} / {}", p.health as i32, p.max_health as i32);
self.draw_text(40, y_row1, &hp_text, [220, 220, 220], 255);
}
let brush_color = brush_color(brush);
self.set(0, y_row2 + 4, '#', brush_color, bg);
let brush_text = format!(" {}", brush_name);
self.draw_text(1, y_row2, &brush_text, [200, 200, 140], 255);
let gear = format!(
"W:[{}] A:[{}] INV:{} FPS:{}",
weapon_glyph,
armor_glyph,
player_state.inventory.len(),
fps as i32
);
let gear_w = Self::text_width(&gear);
let gear_x = (screen_w as i32 - gear_w).max(0);
self.draw_text(gear_x, y_row2, &gear, [160, 180, 220], 255);
let stats = format!(
"LV:{} XP:{} K:{} S:{} D:{} T:{}",
if let Some(p) = player { p.level } else { 0 },
if let Some(p) = player { p.xp } else { 0 },
kills,
score,
depth,
tick
);
let stats_w = Self::text_width(&stats);
let stats_x = (screen_w as i32 - stats_w).max(0);
self.draw_text(stats_x, y_row3, &stats, [180, 190, 220], 255);
if let Some(p) = player {
if p.on_fire {
let msg = "ON FIRE!";
self.draw_text(40, y_top - 7, msg, [255, 100, 20], 255);
}
}
}
pub fn draw_messages(&mut self, x: i32, y: i32) {
let mut yy = y;
let messages: Vec<(String, u32)> = self.messages.iter().rev().take(8).cloned().collect();
for (msg, life) in messages {
if yy < 0 {
break;
}
let fade = (life as f32 / 300.0).clamp(0.3, 1.0);
let fg = [
(200.0 * fade) as u8,
(200.0 * fade) as u8,
(200.0 * fade) as u8,
];
self.draw_text(x, yy, &msg, fg, 255);
yy -= 6;
}
}
pub fn draw_death_screen(&mut self, screen_w: usize, screen_h: usize, kills: u32, score: u32) {
let cx = screen_w as i32 / 2;
let cy = screen_h as i32 / 2;
let msg = "YOU DIED";
let x = cx - (Self::text_width(msg) / 2);
self.draw_text(x, cy - 3, msg, [255, 50, 50], 255);
let stats = format!("KILLS: {} SCORE: {}", kills, score);
let x2 = cx - (Self::text_width(&stats) / 2);
self.draw_text(x2, cy + 3, &stats, [200, 200, 200], 255);
}
pub fn text_width(text: &str) -> i32 {
text.chars()
.map(|c| if c == '\n' { 0 } else { 3 })
.sum::<usize>() as i32
}
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, fg: [u8; 3], alpha: u8) {
let mut cx = x;
for c in text.chars() {
if c == '\n' {
cx = x;
continue;
}
if let Some(bitmap) = char_bitmap(c) {
for row in 0..5 {
for col in 0..3 {
let filled = (bitmap[row] >> (2 - col)) & 1 == 1;
let a = if filled { alpha } else { 0 };
self.set_alpha(cx + col as i32, y + row as i32, c, fg, [0, 0, 0], a);
}
}
} else {
for row in 0..5 {
for col in 0..3 {
self.set_alpha(cx + col as i32, y + row as i32, c, fg, [0, 0, 0], alpha);
}
}
}
cx += 3;
}
}
}
fn brush_color(mat: MaterialId) -> [u8; 3] {
let reg = crate::world::material::MaterialRegistry::instance();
let m = reg.get(mat);
[m.color_fg.0, m.color_fg.1, m.color_fg.2]
}
fn material_name(mat: MaterialId) -> &'static str {
match mat {
MaterialId::Empty => "Erase",
MaterialId::Sand => "Sand",
MaterialId::Water => "Water",
MaterialId::Stone => "Stone",
MaterialId::Lava => "Lava",
MaterialId::Wood => "Wood",
MaterialId::Flesh => "Flesh",
MaterialId::Bone => "Bone",
MaterialId::Steam => "Steam",
MaterialId::Fire => "Fire",
MaterialId::Acid => "Acid",
MaterialId::Smoke => "Smoke",
MaterialId::Grass => "Grass",
MaterialId::Dirt => "Dirt",
MaterialId::Stairs => "Stairs",
}
}
pub fn entity_screen_pos(e: &Entity, cam_x: i32, cam_y: i32) -> (i32, i32) {
let (cx, cy) = e.center();
(cx as i32 - cam_x, cy as i32 - cam_y)
}
pub fn entity_screen_pos_ui(e: &Entity, cam_x: i32, cam_y: i32) -> (i32, i32) {
let (sx, sy) = entity_screen_pos(e, cam_x, cam_y);
(sx * UI_SCALE, sy * UI_SCALE)
}
pub fn entity_class_glyph(e: &Entity) -> char {
match e.kind {
EntityKind::Player if e.alive => '@',
EntityKind::Goblin if e.alive => 'g',
EntityKind::Slime if e.alive => 's',
_ => '%',
}
}
pub fn entity_kind_color(e: &Entity) -> [u8; 3] {
match e.kind {
EntityKind::Player => [200, 180, 255],
EntityKind::Goblin => [160, 240, 120],
EntityKind::Slime => [120, 240, 160],
EntityKind::Corpse => [160, 160, 160],
}
}
fn char_bitmap(c: char) -> Option<[u8; 5]> {
let u = c.to_ascii_uppercase();
match u {
'0' => Some([0b111, 0b101, 0b101, 0b101, 0b111]),
'1' => Some([0b010, 0b010, 0b010, 0b010, 0b010]),
'2' => Some([0b111, 0b001, 0b111, 0b100, 0b111]),
'3' => Some([0b111, 0b001, 0b111, 0b001, 0b111]),
'4' => Some([0b101, 0b101, 0b111, 0b001, 0b001]),
'5' => Some([0b111, 0b100, 0b111, 0b001, 0b111]),
'6' => Some([0b111, 0b100, 0b111, 0b101, 0b111]),
'7' => Some([0b111, 0b001, 0b001, 0b001, 0b001]),
'8' => Some([0b111, 0b101, 0b111, 0b101, 0b111]),
'9' => Some([0b111, 0b101, 0b111, 0b001, 0b111]),
'A' => Some([0b111, 0b101, 0b111, 0b101, 0b101]),
'B' => Some([0b110, 0b101, 0b110, 0b101, 0b110]),
'C' => Some([0b111, 0b100, 0b100, 0b100, 0b111]),
'D' => Some([0b110, 0b101, 0b101, 0b101, 0b110]),
'E' => Some([0b111, 0b100, 0b111, 0b100, 0b111]),
'F' => Some([0b111, 0b100, 0b111, 0b100, 0b100]),
'G' => Some([0b111, 0b100, 0b101, 0b101, 0b111]),
'H' => Some([0b101, 0b101, 0b111, 0b101, 0b101]),
'I' => Some([0b111, 0b010, 0b010, 0b010, 0b111]),
'J' => Some([0b001, 0b001, 0b001, 0b101, 0b111]),
'K' => Some([0b101, 0b101, 0b110, 0b101, 0b101]),
'L' => Some([0b100, 0b100, 0b100, 0b100, 0b111]),
'M' => Some([0b101, 0b111, 0b101, 0b101, 0b101]),
'N' => Some([0b111, 0b101, 0b101, 0b101, 0b101]),
'O' => Some([0b111, 0b101, 0b101, 0b101, 0b111]),
'P' => Some([0b111, 0b101, 0b111, 0b100, 0b100]),
'Q' => Some([0b111, 0b101, 0b101, 0b111, 0b001]),
'R' => Some([0b111, 0b101, 0b110, 0b101, 0b101]),
'S' => Some([0b111, 0b100, 0b111, 0b001, 0b111]),
'T' => Some([0b111, 0b010, 0b010, 0b010, 0b010]),
'U' => Some([0b101, 0b101, 0b101, 0b101, 0b111]),
'V' => Some([0b101, 0b101, 0b101, 0b101, 0b010]),
'W' => Some([0b101, 0b101, 0b101, 0b111, 0b101]),
'X' => Some([0b101, 0b101, 0b010, 0b101, 0b101]),
'Y' => Some([0b101, 0b101, 0b010, 0b010, 0b010]),
'Z' => Some([0b111, 0b001, 0b010, 0b100, 0b111]),
' ' => Some([0b000, 0b000, 0b000, 0b000, 0b000]),
':' => Some([0b000, 0b010, 0b000, 0b010, 0b000]),
'/' => Some([0b001, 0b001, 0b010, 0b100, 0b100]),
'(' => Some([0b001, 0b010, 0b010, 0b010, 0b001]),
')' => Some([0b100, 0b010, 0b010, 0b010, 0b100]),
'[' => Some([0b011, 0b010, 0b010, 0b010, 0b011]),
']' => Some([0b110, 0b010, 0b010, 0b010, 0b110]),
'-' => Some([0b000, 0b000, 0b111, 0b000, 0b000]),
'?' => Some([0b111, 0b001, 0b011, 0b000, 0b010]),
'!' => Some([0b010, 0b010, 0b010, 0b000, 0b010]),
'.' => Some([0b000, 0b000, 0b000, 0b000, 0b010]),
',' => Some([0b000, 0b000, 0b000, 0b010, 0b100]),
'\'' => Some([0b010, 0b010, 0b000, 0b000, 0b000]),
'+' => Some([0b000, 0b010, 0b111, 0b010, 0b000]),
'=' => Some([0b000, 0b111, 0b000, 0b111, 0b000]),
'>' => Some([0b100, 0b010, 0b001, 0b010, 0b100]),
'<' => Some([0b001, 0b010, 0b100, 0b010, 0b001]),
'*' => Some([0b000, 0b101, 0b010, 0b101, 0b000]),
'%' => Some([0b101, 0b001, 0b010, 0b100, 0b101]),
'#' => Some([0b101, 0b111, 0b101, 0b111, 0b101]),
'|' => Some([0b010, 0b010, 0b010, 0b010, 0b010]),
'@' => Some([0b111, 0b101, 0b111, 0b101, 0b111]),
'_' => Some([0b000, 0b000, 0b000, 0b000, 0b111]),
_ => None,
}
}