//! Shared chrome content, independent of output encoding. use std::fmt::Write as _; use crate::app::PresentationSnapshot; use hexwork_ui::logfmt::RecentLogWindow; pub const MELEE_WARNING: &str = "Melee warning: surviving hostiles counterattack only within their own reach."; pub const TERMINAL_CHROME_ROWS: u16 = 8; pub const RECENT_LOG_ROWS: usize = 3; pub fn terminal_line(value: &str, width: usize) -> String { hexwork_ui::text::fit_terminal_cells(value, width) } pub fn chrome_rows(width: usize, presentation: &PresentationSnapshot, context_ribbon: &str, recent_logs: &RecentLogWindow) -> Vec { let hud = hexwork_ui::hud::Hud { hp: presentation.hp.to_string(), light: presentation.light.clone(), burden: presentation.burden.clone(), depth: String::new(), tick: presentation.tick, }; let tactical_status = if presentation.view.is_orthographic() { format!("[TACTICAL Z {} — {}]", presentation.tactical_z, if presentation.wall_clock_paused { "PAUSE-STEP" } else { "LIVE" }) } else { String::new() }; let recent_header = match (recent_logs.omitted_entries, recent_logs.clipped) { (0, false) => "Recent feedback".to_owned(), (0, true) => "Recent feedback [clipped]".to_owned(), (omitted, false) => format!("Recent feedback [{omitted} older omitted]"), (omitted, true) => format!("Recent feedback [{omitted} older omitted; clipped]"), }; let mut base_tail = if tactical_status.is_empty() { format!(" · t{}", presentation.tick) } else { format!(" · t{} · {tactical_status}", presentation.tick) }; if presentation.view.is_orthographic() && presentation.tactical_map.player_cell[2] != presentation.tactical_z { let _ = write!(base_tail, " · you Z {}", presentation.tactical_map.player_cell[2]); } let render_location = |available: usize| -> String { match &presentation.location { hexwork_core::runtime::RunLocation::Town(id) => { let id = hexwork_ui::hud::fit_value(id, available.saturating_sub(5)); terminal_line(&format!("Town {id}"), available) } hexwork_core::runtime::RunLocation::Deep(depth) => terminal_line(&format!("Deep {depth}"), available), hexwork_core::runtime::RunLocation::Uninitialized => terminal_line("Location unavailable", available), } }; // Action-paced locomotion state lives in the shared composed frame, not // rebuilt per frontend. It is appended only when it does not change how the // location renders, so the location keeps its established fit and the pace // label yields first under width pressure (the state stays available on the // presentation snapshot regardless). let base_width = width.saturating_sub(base_tail.chars().count()); let pace_tail = format!("{base_tail} · {}", presentation.pace.label()); let pace_width = width.saturating_sub(pace_tail.chars().count()); let (location, tail) = if render_location(pace_width).trim_end() == render_location(base_width).trim_end() { (render_location(pace_width), pace_tail) } else { (render_location(base_width), base_tail) }; let mut lines = vec![hexwork_ui::hud::render_vitals(&hud, width), terminal_line(context_ribbon, width), terminal_line(MELEE_WARNING, width), terminal_line(&format!("{location}{tail}"), width), terminal_line(&recent_header, width)]; lines.extend(recent_logs.rows.iter().take(RECENT_LOG_ROWS).map(|line| terminal_line(line, width))); while lines.len() < usize::from(TERMINAL_CHROME_ROWS) { lines.push(terminal_line("", width)); } lines } #[cfg(test)] mod tests { use super::*; use crate::app::PresentationSnapshot; use crate::pace::PacePhase; use hexwork_core::runtime::RunLocation; use hexwork_render::camera::{first_person, View}; fn snapshot(pace: PacePhase) -> PresentationSnapshot { let view: View = first_person([0.0, 0.0, 0.0], 0.0, 0.0); PresentationSnapshot { view, screen: hexwork_ui::Screen::Play, tactical_z: 0, tactical_map: hexwork_render::scene::TacticalMapContext { min: [0, 0, 0], max: [0, 0, 0], player_cell: [0, 0, 0], }, hp: 10, location: RunLocation::Town("milthollow".into()), tick: 7, light: "no carried lamp".into(), burden: "within limit".into(), wall_clock_paused: false, look_active: false, camera_yaw: 0.0, camera_tilt: 0.0, pace, } } fn logs() -> hexwork_ui::logfmt::RecentLogWindow { hexwork_ui::logfmt::RecentLogWindow { rows: Vec::new(), omitted_entries: 0, clipped: false, } } #[test] fn pace_state_is_rendered_in_the_shared_chrome_tail() { for (phase, label) in [ (PacePhase::Frozen, "FROZEN"), (PacePhase::Acting(3), "ACTING 3 ticks"), (PacePhase::Live, "LIVE"), (PacePhase::SystemPause, "SYSTEM PAUSE"), (PacePhase::TacticalPause, "TACTICAL PAUSE"), (PacePhase::OverloadPause, "OVERLOAD PAUSE"), ] { let rows = chrome_rows(80, &snapshot(phase), "Esc quit", &logs()); assert!( rows[3].contains(&format!(" · t7 · {label}")), "state {label:?} missing from shared chrome: {}", rows[3] ); } } #[test] fn pace_label_yields_to_a_long_location_without_erasing_it() { let mut s = snapshot(PacePhase::Acting(1)); s.location = RunLocation::Town("Long town identifier ".repeat(30)); let rows = chrome_rows(80, &s, "Esc quit", &logs()); assert!(rows[3].starts_with("Town "), "location erased: {}", rows[3]); assert!( !rows[3].contains("ACTING"), "pace label must yield rather than truncate the location: {}", rows[3] ); } }