status bar display
This commit is contained in:
@@ -94,6 +94,8 @@ pub(crate) fn handle_board_input(
|
||||
KeyCode::Left => try_move_with_transition(game, ui, Direction::West),
|
||||
KeyCode::Right => try_move_with_transition(game, ui, Direction::East),
|
||||
KeyCode::Char('l') => ui.log.toggle(),
|
||||
// Toggle the status sidebar (play-only: this handler is InputMode::Board).
|
||||
KeyCode::Tab => ui.show_status = !ui.show_status,
|
||||
KeyCode::PageUp if log_open => ui.log.scroll_by(-5, game.log.len()),
|
||||
KeyCode::PageDown if log_open => ui.log.scroll_by(5, game.log.len()),
|
||||
_ => {}
|
||||
|
||||
+32
-6
@@ -18,6 +18,7 @@ mod overlay;
|
||||
mod render;
|
||||
mod scroll_overlay;
|
||||
mod speech;
|
||||
mod status;
|
||||
mod term;
|
||||
mod transition;
|
||||
mod ui;
|
||||
@@ -37,6 +38,7 @@ use crate::menu::MenuWidget;
|
||||
use crate::mode::{Mode, PendingMode};
|
||||
use crate::scroll_overlay::ScrollOverlayWidget;
|
||||
use crate::speech::SpeechBubblesWidget;
|
||||
use crate::status::StatusSidebarWidget;
|
||||
use crate::ui::Ui;
|
||||
use kiln_core::game::GameState;
|
||||
use kiln_core::log::LogLine;
|
||||
@@ -330,12 +332,30 @@ fn draw_play(frame: &mut Frame, game: &GameState, ui: &mut Ui) {
|
||||
format!(" {} ", board.name)
|
||||
};
|
||||
|
||||
// Carve a fixed-width status sidebar off the left edge when enabled; the
|
||||
// board/log layout below operates on whatever area is left (`content_area`).
|
||||
let content_area = if ui.show_status {
|
||||
// Overlap by one column so the sidebar's right border and the board's
|
||||
// left border share a cell (both blocks merge their borders).
|
||||
let [sidebar_area, rest] =
|
||||
Layout::horizontal([Constraint::Length(STATUS_SIDEBAR_WIDTH), Constraint::Min(0)])
|
||||
.spacing(Spacing::Overlap(1))
|
||||
.areas(frame.area());
|
||||
frame.render_widget(
|
||||
StatusSidebarWidget::new(game.player_health - 2, game.player_gems),
|
||||
sidebar_area,
|
||||
);
|
||||
rest
|
||||
} else {
|
||||
frame.area()
|
||||
};
|
||||
|
||||
if ui.log.open {
|
||||
// Split the screen: board on top, log panel of `height` at the bottom.
|
||||
// Split the content area: board on top, log panel of `height` at the bottom.
|
||||
let [board_area, log_area] =
|
||||
Layout::vertical([Constraint::Min(3), Constraint::Length(ui.log.height)])
|
||||
.spacing(Spacing::Overlap(1))
|
||||
.areas(frame.area());
|
||||
.areas(content_area);
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(title)
|
||||
@@ -345,15 +365,21 @@ fn draw_play(frame: &mut Frame, game: &GameState, ui: &mut Ui) {
|
||||
draw_board_area(frame, inner, game, ui);
|
||||
frame.render_stateful_widget(LogWidget::new(&game.log), log_area, &mut ui.log);
|
||||
} else {
|
||||
// Panel closed: board fills the screen, latest message in the border.
|
||||
let mut block = Block::bordered().title(title);
|
||||
// Panel closed: board fills the content area, latest message in the border.
|
||||
// Merge borders so a joined edge with the status sidebar renders cleanly.
|
||||
let mut block = Block::bordered()
|
||||
.title(title)
|
||||
.merge_borders(MergeStrategy::Exact);
|
||||
block = block.title_bottom(log_preview_line(&game.log).left_aligned());
|
||||
let inner = block.inner(frame.area());
|
||||
frame.render_widget(block, frame.area());
|
||||
let inner = block.inner(content_area);
|
||||
frame.render_widget(block, content_area);
|
||||
draw_board_area(frame, inner, game, ui);
|
||||
}
|
||||
}
|
||||
|
||||
/// Width (in terminal columns, borders included) of the play-mode status sidebar.
|
||||
const STATUS_SIDEBAR_WIDTH: u16 = 18;
|
||||
|
||||
/// Renders the inner board area: either the active board-layer animation or the live board.
|
||||
///
|
||||
/// If `ui.pending_transition` is set, pre-renders both boards into a new
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//! Play-mode status sidebar widget for kiln-tui.
|
||||
//!
|
||||
//! [`StatusSidebarWidget`] draws the player's persistent stats (health + gems)
|
||||
//! in a bordered block on the left of the screen during play. It is purely
|
||||
//! presentational: it reads `health`/`gems` values handed in at construction and
|
||||
//! never mutates game state.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::symbols::merge::MergeStrategy;
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Paragraph, Widget};
|
||||
|
||||
/// How many hearts the health row always shows (the player's max health).
|
||||
const MAX_HEARTS: u32 = 5;
|
||||
/// A filled heart: bright red.
|
||||
const HEART_FULL: Color = Color::Rgb(255, 0, 0);
|
||||
/// A depleted heart: dark red.
|
||||
const HEART_EMPTY: Color = Color::Rgb(90, 0, 0);
|
||||
/// The gem glyph color: blue.
|
||||
const GEM: Color = Color::Rgb(80, 80, 255);
|
||||
|
||||
/// A ratatui widget that renders the play-mode status sidebar.
|
||||
///
|
||||
/// Shows a `Health:` label above a row of [`MAX_HEARTS`] heart glyphs (the first
|
||||
/// `health` filled bright red, the rest dark red) and a `Gems: ♦ N` line.
|
||||
pub struct StatusSidebarWidget {
|
||||
/// The player's current health, clamped to [`MAX_HEARTS`] when drawn.
|
||||
health: u32,
|
||||
/// The number of gems collected, shown as a count.
|
||||
gems: u32,
|
||||
}
|
||||
|
||||
impl StatusSidebarWidget {
|
||||
/// Builds a sidebar widget for the given player stats.
|
||||
pub fn new(health: u32, gems: u32) -> Self {
|
||||
Self { health, gems }
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for StatusSidebarWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// One bright-red span per filled heart, dark-red for the rest, so a
|
||||
// partly-depleted bar reads at a glance.
|
||||
let filled = self.health.min(MAX_HEARTS);
|
||||
let hearts: Vec<Span> = (0..MAX_HEARTS)
|
||||
.map(|i| {
|
||||
let color = if i < filled { HEART_FULL } else { HEART_EMPTY };
|
||||
Span::styled("♥", Style::default().fg(color))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let lines = vec![
|
||||
Line::from("Health:"),
|
||||
Line::from(hearts),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::raw("Gems: "),
|
||||
Span::styled("♦", Style::default().fg(GEM)),
|
||||
Span::raw(format!(" {}", self.gems)),
|
||||
]),
|
||||
];
|
||||
|
||||
// A bordered block; `merge_borders` joins its right edge with the board
|
||||
// block's left edge where the layout overlaps them by one column.
|
||||
let block = Block::bordered()
|
||||
.title(" Status (tab) ")
|
||||
.merge_borders(MergeStrategy::Exact);
|
||||
Paragraph::new(lines).block(block).render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,9 @@ pub(crate) struct Ui {
|
||||
/// Set when `Esc` is pressed during a playtest, signalling the run loop to drop the
|
||||
/// playtest game and restore [`suspended_editor`](Ui::suspended_editor).
|
||||
pub(crate) exit_playtest: bool,
|
||||
/// Whether the play-mode status sidebar (player health + gems) is visible.
|
||||
/// Toggled with `i` while playing; defaults to `true`. Has no effect in the editor.
|
||||
pub(crate) show_status: bool,
|
||||
}
|
||||
|
||||
impl Default for Ui {
|
||||
@@ -59,6 +62,7 @@ impl Default for Ui {
|
||||
pending_mode: None,
|
||||
suspended_editor: None,
|
||||
exit_playtest: false,
|
||||
show_status: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user