use std::fmt::Write as _;
use tixel::utils::write_fg_color;
use tixel::{
Color, HalfCellCanvas,
utils::{write_bg_color, write_move_to},
};
use super::game::{CollectibleKind, Game, SKY_HEIGHT, Tile, TileKind};
fn collectible_color(collectible: CollectibleKind) -> Color {
match collectible {
CollectibleKind::Silver => (170, 170, 170).into(),
CollectibleKind::Gold => (240, 240, 10).into(),
CollectibleKind::Diamond => (150, 250, 250).into(),
CollectibleKind::Ruby => (240, 10, 10).into(),
CollectibleKind::Fuel => (10, 240, 10).into(),
CollectibleKind::Time => (50, 70, 250).into(),
}
}
fn tile_color(tile: Tile) -> (u8, u8, u8) {
match tile {
Tile::Visible(tile_kind) => match tile_kind {
TileKind::Ground { depth, noise } => {
let depth_mod = depth as f64 / 4.;
let noise_mod = 8. * noise;
let r = (70. + depth_mod + noise_mod).max(30.);
let g = (55. + depth_mod + noise_mod).max(25.);
let b = (40. + depth_mod + noise_mod).max(20.);
(r as u8, g as u8, b as u8)
}
TileKind::Sky { noise } => {
let color_mod = (20. * noise).max(0.);
let r = 130. + color_mod;
let g = 200. + color_mod;
let b = 230. + color_mod;
(r as u8, g as u8, b as u8)
}
TileKind::Tunnel => (10, 10, 10),
TileKind::Collectible { kind } => {
let Color::Rgb(r, g, b) = collectible_color(kind) else {
unreachable!()
};
(r, g, b)
}
TileKind::Rock { noise } => {
let noise_mod = 8. * noise;
let r = (40. + noise_mod).max(30.);
let g = (40. + noise_mod).max(25.);
let b = (45. + noise_mod).max(20.);
(r as u8, g as u8, b as u8)
}
},
Tile::Fog { noise, .. } => {
let noise_mod = 8. * noise;
let r = 22. + noise_mod;
let g = 17. + noise_mod;
let b = 12. + noise_mod;
(r as u8, g as u8, b as u8)
}
}
}
fn lerp_u8(from: u8, to: u8, t: f64) -> u8 {
(from as f64 * (1. - t) + to as f64 * t) as u8
}
fn faded(color: (u8, u8, u8), clear: (u8, u8, u8), opacity: f64) -> Color {
if opacity >= 1. {
return Color::Rgb(color.0, color.1, color.2);
}
Color::Rgb(
lerp_u8(clear.0, color.0, opacity),
lerp_u8(clear.1, color.1, opacity),
lerp_u8(clear.2, color.2, opacity),
)
}
pub struct Renderer {
scroll: i64,
}
impl Renderer {
pub fn new() -> Self {
Self { scroll: 0 }
}
fn update_scroll(&mut self, game: &Game, canvas: &HalfCellCanvas) {
self.scroll = self.scroll.min(game.pos().1 - SKY_HEIGHT);
self.scroll = self
.scroll
.max(game.pos().1 + SKY_HEIGHT - canvas.height() as i64);
}
#[expect(clippy::too_many_arguments)]
pub fn render(
&mut self,
canvas: &mut HalfCellCanvas,
game: &mut Game,
output: &mut String,
offset: (usize, usize),
show_hud: bool,
opacity: f64,
clear: (u8, u8, u8),
) {
self.update_scroll(game, canvas);
let window_top_y = self.scroll;
let window_bot_y = window_top_y + canvas.height() as i64;
for y in window_top_y..window_bot_y {
for x in 0..(canvas.width() as i64) {
let color = tile_color(game.get_tile((x, y)));
canvas.set_color(
x as usize,
(y - window_top_y) as usize,
faded(color, clear, opacity),
);
}
}
canvas.set_color(
game.pos().0 as usize,
(game.pos().1 - window_top_y) as usize,
faded((250, 250, 250), clear, opacity),
);
output.push_str(&canvas.render());
let top_offset = (offset.0, offset.1.saturating_sub(1));
let bottom_offset = (offset.0, offset.1 + canvas.height() / 2);
if show_hud {
render_top_ui(output, game, top_offset, canvas.width());
render_bottom_ui(output, game, bottom_offset, canvas.width());
} else {
clear_row(output, top_offset, canvas.width());
clear_row(output, bottom_offset, canvas.width());
}
}
}
fn clear_row(buf: &mut String, offset: (usize, usize), width: usize) {
write_move_to(buf, offset.0, offset.1);
write_bg_color(buf, (0, 0, 0).into());
let _ = write!(buf, "{}", " ".repeat(width));
}
const TIME_START_X: usize = 28;
fn render_top_ui(buf: &mut String, game: &Game, offset: (usize, usize), width: usize) {
write_move_to(buf, offset.0, offset.1);
let (fuel_gauge, fuel_gauge_len) = render_fuel_gauge(game.fuel_proportion());
let _ = write!(buf, "{fuel_gauge}");
let _ = write!(buf, "{}", " ".repeat(TIME_START_X - fuel_gauge_len));
let time = game.remaining_time().as_secs();
let mins = time / 60;
let secs = time - 60 * mins;
let time_len = mins.to_string().len() + 3; write_fg_color(buf, (250, 250, 250).into());
let _ = write!(buf, "{mins}:{secs:0>2}");
let depth = game.current_depth();
let depth_str = format!("↓{depth} ");
let depth_len = depth.to_string().len() + 3;
let bank = game.bank_value();
let bank_str = format!("{bank} P ");
let bank_len = bank_str.len();
write_bg_color(buf, (0, 0, 0).into());
let _ = write!(
buf,
"{}",
" ".repeat(width.saturating_sub(TIME_START_X + time_len + depth_len + bank_len))
);
write_fg_color(buf, (230, 230, 230).into());
let _ = write!(buf, "{depth_str}{bank_str}");
}
fn render_bottom_ui(buf: &mut String, game: &Game, offset: (usize, usize), width: usize) {
write_move_to(buf, offset.0, offset.1);
write_fg_color(buf, (250, 250, 250).into());
let message = game.message().unwrap_or_default();
let message_len = message.len() + 1;
let _ = write!(buf, " {message}");
let mut inventory = String::new();
let mut inventory_len = 0;
for collectible in [
CollectibleKind::Silver,
CollectibleKind::Gold,
CollectibleKind::Fuel,
CollectibleKind::Diamond,
CollectibleKind::Ruby,
] {
if let Some(c) = game.inventory().get(&collectible) {
write_fg_color(&mut inventory, collectible_color(collectible));
let _ = write!(&mut inventory, "■");
write_fg_color(&mut inventory, (250, 250, 250).into());
let _ = write!(&mut inventory, " {c} ");
inventory_len += 3 + c.to_string().len();
}
}
let inv_size = game.inventory_size();
let cap = game.inventory_capacity();
let cap_str = format!("({inv_size}/{cap}) ");
let cap_len = cap_str.len();
write_bg_color(buf, (0, 0, 0).into());
let _ = write!(
buf,
"{}",
" ".repeat(width.saturating_sub(inventory_len + message_len + cap_len))
);
let _ = write!(buf, "{inventory}{cap_str}");
}
const FUEL_GAUGE_WIDTH: usize = 8;
const EIGHTHS: [char; 8] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
fn render_fuel_gauge(fuel_proportion: f64) -> (String, usize) {
let mut out = String::new();
let color = Color::Rgb(
(250. * (1. - fuel_proportion)) as u8,
(250. * fuel_proportion) as u8,
10,
);
write_fg_color(&mut out, color);
if fuel_proportion <= 0. {
write_bg_color(&mut out, (40, 40, 40).into());
let _ = write!(&mut out, "{}", " ".repeat(FUEL_GAUGE_WIDTH));
} else if fuel_proportion >= 1. {
let _ = write!(&mut out, "{}", "█".repeat(FUEL_GAUGE_WIDTH));
} else {
let cell_width = fuel_proportion * FUEL_GAUGE_WIDTH as f64;
let full_tiles = cell_width.floor() as usize;
let full_tiles_str = "█".repeat(full_tiles);
let eighth_idx = (cell_width.fract() * 8.).floor() as usize;
let eighth = EIGHTHS[eighth_idx];
let space = " ".repeat(FUEL_GAUGE_WIDTH.saturating_sub(full_tiles + 1));
write_bg_color(&mut out, (40, 40, 40).into());
let _ = write!(&mut out, "{full_tiles_str}{eighth}{space}");
}
write_fg_color(&mut out, (250, 250, 250).into());
write_bg_color(&mut out, (0, 0, 0).into());
let text = format!("{:.0}%", fuel_proportion * 100.);
let _ = write!(&mut out, " {text}");
(out, FUEL_GAUGE_WIDTH + text.len() + 1)
}